How To Use The Twitter API To Find Events

Recap: We've built a rudimentary search function that returns the text of the first matching tweet and the ability to match various keywords to the posts tweeted to us at @SouthBotFunWest. What's left? Linking up the two.

As our goal, let's try to match a basic category of party to a related search. In our case, that's interactive. Let's update our code so that every time someone tweets the word "interactive" at us, we log the text of the first related tweet to to terminal. While we're at it, let's make sure to capture and pass to the search() function the person we've identified is tweeting at us. For the instances where someone is tweeting at us just to say hi, we can go ahead and post our standard response straight away. As ever, we need a return to break us out once we post a tweet so that we don't double post.

var stream = twitter.stream('statuses/filter', { track: '@SouthBotFunWest' })

stream.on('tweet', function (tweet) {
  var asker = tweet.user.screen_name;
  var text = tweet.text;
  var wordArray = tokenizer.tokenize(text);

  // RegExes
  var greetingRE = /^hi$/;
  var interactiveRE = /^interactive$/
  for(var i=0;i < wordArray.length;i++) {
    if (interactiveRE.test(wordArray[i])) {
      search("interactive", asker);
      return;
    } else if (greetingRE.test(wordArray[i])) {
      post("Sup " + "@" + asker + " . So, I've heard about some cool South-by parties. You know, whatever [music, interactive, film, free, food, drink]");
      return;
    } else {
    }
  }

})

Running this code, you'll quickly see a big problem: A string where hi comes before interactive will always trigger the greeting regular expression first and end the loop (since the tweet is evaluated word-by-word). That's definitely an issue! What we really want here is a function that will take a regular expression and the text its matching it against, then search over the word array and return true if there's a match and false if, by the end of the loop, no match turns up.

function matchRE (re, text) {
var wordArray = tokenizer.tokenize(text);
  for(var i=0;i < wordArray.length;i++) {
    if (re.test(wordArray[i])) {
      return true;
    }
  }
  return false; 
}

Let's update our code to not only reflect this new function, but an approach the matches keywords beyond "interactive":

var stream = twitter.stream('statuses/filter', { track: '@SouthBotFunWest' })

stream.on('tweet', function (tweet) {
  var asker = tweet.user.screen_name;
  var text = tweet.text;

  // RegExes
  var greetingRE = /^hi$/;
  var musicRE = /^music$/;
  var interactiveRE = /^interactive$/;
  var filmRE = /^film$/;
  var foodRE = /^food$/;
  var drinkRE = /^drink$/;

  if (matchRE(interactiveRE, text)) {
    console.log("interactive")
  } else if (matchRE(filmRE, text)) {
    console.log("film", text)
  } else if (matchRE(musicRE, text)) {
    console.log("music")
  } else if (matchRE(drinkRE, text)) {
    console.log("drink");
  } else if (matchRE(foodRE, text)) {
    console.log("food")
  } else if (matchRE(greetingRE, text)) {
    console.log("greeting");
  } else {
  }

})

Here are the results that are produced from feeding different text inputs into this logic tree:

hi @SouthBotFunWest, where's a cool party? => "greeting"

hi @SouthBotFunWest, where's a cool music party? => "music"

@SouthBotFunWest! Where's some food? => "food"

That looks perfect (for a first iteration, at least). It's important to note how essential the order is: if the "greeting" response is one of the first options in the if / else if / else sequence, as opposed to the last, it will consistently trigger and prevent the more topic-specific searches later down the chain from ocurring.

The final tasks before us are linking up the regex matches to related searches and returning something from the seach function that we can tweet back at whoever's asking for information. Let's work backwards and start with the latter task first.

Twitter, it turns out, has some seriously nifty data fields in the tweet objects it returns through its search API. One is the urls attribute, which contains an array featuring different versions of any links referenced in the post (including a shortened version, the version Twitter displays in the tweet, and others). By saving the links referenced in the tweets we return in our search, which almost invariably point to RSVP pages or lists of related parties, we can provide them in addition to our text response.

Of course not all posts will contain links, but those are the only ones we're interested in. That's why we need to modify our search() function to return 10 tweets, then iterate over the results to select one that has elements in its urls array. I've put that loop in an else block, because nine times out of ten the first result, in the if block, will be something that we can use.

Let's write the code to take the first tweet with a link, exit the loop, and post that link

function search (query, asker) {
  var search = "SXSW party " + query + " filter:links";
  twitter.get('search/tweets', { q: search, count: 10 }, function(err, data, response) {

    var resultLink; 

    if (data.statuses[0].entities.urls.length > 0) {
      resultLink = data.statuses[0].entities.urls[0].url;
    } else {
      for (var i=0;i < data.statuses.length;i++) {
        if (data.statuses[i].entities.urls.length > 0) {
          resultLink = data.statuses[i].entities.urls[0].url;
          i = data.statuses.length;
        }
      }
    };    

    var result = "@" + asker + " Cool cool. Totally get that... " + query + " is neat. How about this? " + resultLink;
    post(result);
  })
}

That's it. All we need to do now is hook everything up by placing a search() function with the appropriate parameters within our regular expression logic tree. Here's the full final index.js file:

var Twit = require('twit');
var twitInfo = require('./config.js');
var twitter = new Twit(twitInfo);
var natural = require('natural'),
  tokenizer = new natural.WordTokenizer();

function matchRE (re, text) {
  var wordArray = tokenizer.tokenize(text);
  for(var i=0;i < wordArray.length;i++) {
    if (re.test(wordArray[i])) {
      return true;
    }
  }
  return false; 
}

function search (query, asker) {
  var search = "SXSW party " + query + " filter:links";
  twitter.get('search/tweets', { q: search, count: 10 }, function(err, data, response) {

    var resultLink; 

    if (data.statuses[0].entities.urls.length > 0) {
      resultLink = data.statuses[0].entities.urls[0].url;
    } else {
      for (var i=0;i < data.statuses.length;i++) {
        if (data.statuses[i].entities.urls.length > 0) {
          resultLink = data.statuses[i].entities.urls[0].url;
          i = data.statuses.length;
        }
      }
    };    

    var result = "@" + asker + " Cool cool. Totally get that... " + query + " is neat. How about this? " + resultLink;
    post(result);
  })
}

function post (content) {
  twitter.post('statuses/update', { status: content }, function(err, data, response) {
  })
}

var stream = twitter.stream('statuses/filter', { track: '@SouthBotFunWest' })

stream.on('tweet', function (tweet) {
  var asker = tweet.user.screen_name;
  var text = tweet.text;

  // RegExes
  var greetingRE = /^hi$/;
  var musicRE = /^music$/;
  var interactiveRE = /^interactive$/;
  var filmRE = /^film$/;
  var foodRE = /^food$/;
  var drinkRE = /^drink$/;

  if (matchRE(interactiveRE, text)) {
    search("interactive", asker)
  } else if (matchRE(filmRE, text)) {
    search("film", asker)
  } else if (matchRE(musicRE, text)) {
    search("music", asker);
  } else if (matchRE(drinkRE, text)) {
    search("drink", asker)
  } else if (matchRE(foodRE, text)) {
    search("food", asker)
  } else if (matchRE(greetingRE, text)) {
    post("Hey " + "@" + asker + " . So, I've heard about some cool South-by parties. Or you know, whatever. [music, interactive, film, free, food, drink]");
  } else {
  }

})

Now for the moment of truth: If we reach out to our bot with our script running, asking it for a super sweet South-by rec, it responds with ...

... complete disaffection. It's enough to make any father proud!

Deployment

So how would we go about actually setting up this script to run continously? Luckily for us Node has an excellent module for that — forever — that couldn't be easier to use.

Simply install it on your Node-capable machine (note that unless you use a different module, forever needs to be installed globally)...

sudo npm install -g forever

Then start your script with:

forever start index.js

That's it! You might get a warning or two, but if you enter forever list you should see your process, along with its current uptime.

Parting Thoughts

There are a lot of ways we could improve the current setup (check out github to see some extra tweaks I've made), but this fulfills our original goal — to build a bot that can respond to incoming tweets with a cheeky response and a link to an appropriate party.

If you're interested in another Node Twitterbot or a very gentle introduction to penetration testing, via XSS, check out another project of mine, bughunting.guide.

Otherwise, thanks for reading and I'll see you at SXSWi!

This article was reprinted with permission from Airpair. Any opinions expressed in this article do not necessarily reflect the views of ProgrammableWeb or its editorial staff. If you have questions regarding the information in the article, please contact the author.

Be sure to read the next Application Development article: How StoryCorps.me Was Built Using The Wordpress API and PhoneGap

 

Comments (0)