Streaming Twitter Bot in Node

Over the years I've played a lot with Twitter and different bots and interactions. Shoot, there are write-ups on this blog about authenticating raw Twitter requests with OAuth, extending that to handling Twitter streams with Guzzle, and then doing it again with raw streaming requests. Using these tools and learnings I've created a few little bots, some handling realtime interactions and others more basic, all of which are using PHP. Maybe it's time to look into another language.

First, a disclaimer. When I set out to build this latest bot I really wanted to use ReactPHP. This event-driven framework is pretty darn awesome and seems to be perfect for handling a streaming API. However, it only handles HTTP/1.0, doesn't do chunked-transfer, has no OAuth baked into the http-client, and is very much beta. I didn't feel like diving that deep, not again, so instead went with Node. And there's an awesome client already built in Node that made building a bot stupid easy.

Twit is a Twitter API client written in Node that handles both REST and streaming endpoints. It seems to be fairly mature, having been around for four years. After installing it with npm, setting up a basic bot is simple as pulling in twit and instantiating with your credentials.

  1. var twit = require('twit');

  2. var bot = new twit({

  3. consumer_key: 'CONSUMER KEY',

  4. consumer_secret: 'CONSUMER SECRET',

  5. access_token: 'ACCESS TOKEN',

  6. access_token_secret: 'ACCESS TOKEN SECRET'

  7. });

Now, with this bot I wanted to automatically respond to some tweets with a website. I'm not entirely sure if the recipients should be constrained to only followers of the bot or if there should be any filtering to avoid spammy behavior, but for a proof of concept I have it listening to an open stream. Basically, any time someone tweets that they deserve a cookie, the bot will send yougetacookie.com at them. If they say someone else deserves a cookie, then someone else gets sent the link.

Aside: yougetacookie.com is a little project I built some four or five years ago as a joke. It drops a cookie on your browser, with values like 'chocolate chip' or 'M&M', and then on refresh it will scold you for trying to 'consume' too many cookies and ruining your dinner. Mild internet humor.

Anyways, Twit has some syntactic sugar when it comes to dealing with streaming endpoints. Streaming Twitter endpoints have a few special messages that are mixed in with the normal tweets, like limit notices and warnings, that are triggered as events by the client. This makes it very easy break up the code or chain things along without worrying about writing the logic to detect anomalies in the stream.

  1. bot

  2. .stream('statuses/filter', {

  3. language: 'en',

  4. track: [

  5. 'should have cookie',

  6. 'deserve cookie'

  7. ]

  8. })

  9. .on('connect', function (request) {

  10. console.log('Connecting...');

  11. })

  12. .on('connected', function (response) {

  13. console.log('Connected!');

  14. })

  15. .on('limit', function (message) {

  16. console.log('Limit notification received');

  17. console.log(message);

  18. });

This chunk of code opens up a streaming endpoint and spits out some debugging information along the way. The endpoint itself, 'statuses/filter', is a basic search endpoint for all public tweets. Technically the 'track' field is a comma-separated parameter in the request, but Twit will convert the array for you.

Several of the events respond with contextual data in the callback. The 'connect' event will return the outgoing request to Twitter, and the 'connected' with the response information from the connection. Both of these can be very useful for debugging. I decided not to fill out my logs too much in the early stages of the bot, though later on I may add in some response codes and authentication errors in case things start acting weird.

Once the base is built out it's time to actually handle the incoming tweets. These are just different events coming in, so all we need to do is listen for them and pass them onto a callback.

  1. bot

  2. ...

  3. .on('tweet', function (tweet) {

  4. console.log('Tweet received: ' + tweet.text);

  5. if (tweet.retweeted) {

  6. console.log('Is retweet - ignoring');

  7. return;

  8. }

  9. if (

  10. tweet.text.match(/REGEX/i) &&

  11. tweet.in_reply_to_screen_name

  12. ) {

  13. console.log('Sending tweet to mentioned user');

  14. bot.push('statuses/update', {

  15. status: 'TWEET HERE',

  16. in_reply_to_status_id: tweet.id

  17. }, function (error, data, response) {

  18. // add in logging here

  19. });

  20. return;

  21. }

  22. if (

  23. tweet.text.match(/REGEX/i) &&

  24. !tweet.in_reply_to_screen_name

  25. ) {

  26. console.log('Sending tweet to original author');

  27. bot.post('statuses/update', {

  28. status: 'TWEET HERE',

  29. in_reply_to_status_id: tweet.id

  30. }, function (error, data, response) {

  31. // add in logging here

  32. });

  33. return;

  34. }

  35. console.log('Unmatched tweet, ignoring');

  36. });

Regex matching is a necessary evil. Twitter search endpoints perform basic queries and will return a lot of collateral. There is no exact phrase matching or anything, so for this bot I needed to double check that the responses received deserve action. Plus, as defined earlier, this bot will either send a message to the author or a mentioned user, so I needed to figure out if the author deserved the cookie or if the author thinks someone else deserves one.

Once beyond the boring matching is something more fun, the actual update itself. I'm able to reuse the bot object inside through the magic of javascript scope and just have it issue a simple update. Now, I've tried this before with some of my streaming PHP bots and was never able to get it to work. I'm not sure how Twit handles things differently - in PHP I had to instantiate two separate clients with two separate sets of credentials.

And that's it. 81 lines of liberally spaced javascript and a streaming bot that issues two different types of responses is born. As much as I like the PHPs this was pretty easy. I may end up writing future bots with node if the upkeep of this little one is simple enough.