blog

Nanobot_art

Nanobot: A Tiny Little Twitterbot Framework

by

I’ve written a few posts here in the past on twitterbots — little bits of code that can generate and respond to tweets. Since those posts were published, I’ve traded messages with a few people who used the code for my original bot to write their own, and when I recently went back to the original post I noticed this at the end:

Now that I’ve built this and seen it running, I can imagine extracting the underlying logic for this into a little twitterbot framework so that next time I get a weird urge to do something like this and a few hours that I have nothing better to do with, I can make another bot quickly.

I pulled together a few hours this past week and did exactly that, creating a Python twitterbot framework that I’m calling ‘nanobot’.

Grow the nanobots up
Grow them in the cracks in the sidewalk
Wind the nanobots up
Wind them up and wish them away
— They Might Be Giants

What The Framework Does For You

The bots generated with this framework are little command-line apps that are meant to be invoked periodically (mine are run once a minute by cron) and each time they run:

  • decide whether or not to generate a tweet (and if so, generate it)
  • look to see if any users have @mentioned it, and if so, do something (by default, a bot written with nanobot will like any tweet that mentions it)
  • handle any events that were received via Twitter’s streaming API
  • send any tweets that were created as a result of the above actions.

The core logic that runs all of those steps remains constant, so custom bots only need to add the small bits that make them unique.

What You Need to Add

Factoring all of the common logic out into a framework means that your bot only needs to implement a small bit of code that makes it do its unique thing. The Tockbot demo that’s included here is only around 100 lines long, and about 25% of those lines are comments and docstrings.

Twitter Setup

Obviously, before you can do anything, you need to create a new Twitter account and Twitter application for your bot to use. The instructions regarding this in my original post are still on point here, so consult that for more information.

Derive a Python class from nanobot.Nanobot

The nanobot code comes with a quick demo bot called TockBot. It will generate a tweet each hour at the top of the hour, and will reply to any @mention that includes the word ‘tick’ with the current time.

Create Tweets

First, the bot needs to decide if it should generate a tweet, which happens in the method IsReadyForUpdate(). There’s a default version of this built into the framework that uses the logic from my tmbotg bot:

If:

  • a random floating point number is less than a configurable tweet probability value
  • …and it’s been at least a configurable number of minutes since our last tweet (don’t send them too frequently)
  • …or it’s been more than some configurable number of minutes since our last tweet (don’t stay quiet for too long)
  • or the bot was launched with the --force command line argument

…then we generate a new tweet.

The Tockbot has its own logic: If the current minute is zero (top of the hour) or we were invoked with --force, it’s time to tweet.

   def IsReadyForUpdate(self):
      '''
         Overridden from base class.
         We're ready to create an update when it's the top of the hour,
         or if the user is forcing us to tweet.
      '''
      now = datetime.now()
      return 0 == now.minute

When it’s time to make a tweet, the framework will call your CreateUpdateTweet() method, which does whatever it needs to do to create some text that’s a tweetable length, and then adds a dict with that text as the value for a key named status to the object’s list of tweets:

   def CreateUpdateTweet(self):
      ''' Chime the clock! '''
      now = datetime.now()
      # figure out how many times to chime; 1x per hour.
      chimeCount = (now.hour % 12) or 12
      # create the message to tweet, repeating the chime
      # NowString() defined elsewhere, it just formats the
      # current time.
      msg = "{0}\n\n{1}".format("\n".join(["BONG"] * chimeCount),
         NowString(now))
      # add the message to the end of the tweets list
      self.tweets.append({'status': msg})
      # add an entry to the log file.
      self.Log("Tweet", ["{} o'clock".format(chimeCount)])

tockbot_tweet

Handle a Mention

If other users @mention your bot’s account, the framework will pass a dict with the data representing that mention to your HandleOneMention() method. The default handler for this just likes/favorites each mention, but we’d like to do more here. If someone mentions the Tockbot and includes the word ‘tick’, we’ll also reply to them with the current time:

   def HandleOneMention(self, mention):
      ''' Like the tweet that mentions us. If the word 'tick' appears
         in that tweet, also reply with the current time.
      '''
      who = mention['user']['screen_name']
      text = mention['text']
      theId = mention['id_str']
      eventType = "Mention"
      # we favorite every mention that we see
      if self.debug:
         print "Faving tweet {0} by {1}:\n {2}".format(theId, who,
            text.encode("utf-8"))
      else:
         self.twitter.create_favorite(id=theId)
      if 'tick' in text.lower():
         # reply to them with the current time.
         now = datetime.now()
         replyMsg = "@{0} {1}".format(who, NowString(now))
         if self.debug:
            print "REPLY: {}".format(replyMsg)
         else:
            self.tweets.append({'status': replyMsg, 'in_reply_to_status_id': theId})
         eventType = "Reply"
      self.Log(eventType, [who])

tockbot_reply

Handle Streaming API Events

As we discussed in this earlier post, much of the data that Twitter provides isn’t available through their REST API, only via a real-time streaming API. If you launch an instance of your bot using the --stream command line argument, it will connect to that streaming API and sit forever waiting for streaming events to be sent for it to process.

When this stream-handling instance of the bot receives a message containing event data, it writes the message out into a file with a unique name and the extension .stream. The next time that your bot is launched periodically, part of its general processing flow will look to see if there are any files with that extension; if there are, it attempts to find a handler function in your bot for that event type, and if it finds one, will call that handler with the event data.

The event types that Twitter supports at the time of writing are: access_revoked, block, unblock, favorite, unfavorite, follow, unfollow, list_created, list_destroyed, list_updated, list_member_added, list_member_removed, list_user_subscribed, list_user_unsubscribed, quoted_tweet, and user_update. You can find details on the purpose and content of each at https://dev.twitter.com/node/201.

Your handler method needs to be named using the pattern Handle_[event_name]. Our example bot only looks for events of type quoted_tweet, which we treat like an @mention — if someone quotes one of our tweets, we like that tweet:

   def Handle_quoted_tweet(self, data):
      '''Like any tweet that quotes us. '''
      tweetId = data['target_object']['id_str']
      if self.debug:
         print "Faving quoted tweet {0}"
         .format(tweetId)
      else:
         try:
            self.twitter.create_favorite(id=tweetId)
         except TwythonError as e:
            self.Log("EXCEPTION", str(e))

Customize Your Expected Configuration

Nanobot bots get their runtime configuration from a text file containing JSON data. To simplify the creation of this file, if you run a bot that can’t load its config file, it will create a new file that has placeholder data in the correct format so you can edit an existing file instead of worrying about creating a new one with all the correct key names, etc. To add default key/value pairs that are specific to your bot, override the base class method GetDefaultConfigOptions() to return a dict that contains your default configuration data.

Add Custom Command Line Flags

The framework supports three command line options:

  • --debug: Don’t generate any twitter output, just print to the console for debugging.
  • --force: Override the bot’s logic to decide whether to generate a tweet.
  • --stream: Make this instance of the bot listen to the Twitter streaming API instead of executing its regular logic.

If your bot needs additional command line arguments, create a function that accepts an argparser.ArgumentParser object, and pass it to the GetBotArguments() function as part of startup. Your function can call any of the methods that the ArgumentParser object supports.

Get Your Bot Running

The framework defines a code>@classmethod called CreateAndRun() that accepts a dict of arguments to pass to the bot, and attempts to launch it. If you’ve created a function to add additional command line arguments, the source file for you bot will end with code like:

      def MyArgAdder(parser):
         parser.add_argument(...)
      if __name__ == "__main__":
         MyCoolBot.CreateAndRun(GetBotArguments(MyArgAdder))

If you don’t need any additional arguments, just omit that argAdder bit.

Why You Might Not Want To Use This

I don’t know that I’d use this framework as-is to implement a realtime conversational interface like the cool kids are talking about this year. The challenges there are a little different than I’ve focused on here.

I definitely wouldn’t use this framework to write bots intended to spam people with unwanted sales pitches — not because it’s not suited to that, but because PLEASE DON’T DO THAT WITH MY FRAMEWORK.

I don’t know that I agree 100% with all of the points in this post by Darius Kazemi on bot ethics, but if you’re going to write a bot, please consider the impact that it might have. Make something that amuses and delights people, not something that annoys people without them soliciting it.

Get The Code

The code is on github.

Once you’ve cloned the repository, you can install it locally (probably into a virtualenv) with the standard Python python setup.py install. It’s not yet available through the Cheeseshop, but I’ll probably upload it there at some point so it can be installed properly through pip.

If you build something using this, please reach out on twitter @bgporter and point me at your bot. I’ve enjoyed seeing what other folks have already done with earlier versions of this that weren’t as easy to work with.

+ more