Nanobot: A Tiny Little Twitterbot Framework

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.

[code language=”python”]
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
[/code]

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:

[code language=”python”]
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)])

[/code]

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:

[code language=”python”]
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])
[/code]

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:

[code language=”python”]
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))
[/code]

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

@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:

[code language=”python”]
def MyArgAdder(parser):
parser.add_argument(…)

if __name__ == "__main__":
MyCoolBot.CreateAndRun(GetBotArguments(MyArgAdder))
[/code]

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.

Brett g Porter

Brett g Porter

Lead Engineer, Audio+Music at Art+Logic
Lead Engineer, Audio+Music development at Art+Logic. Always looking for excuses to write code. Tweets at both @artandlogic and @bgporter.
Brett g Porter

@bgporter

Music+Software+Music Software+Ice Cream. Relapsing composer/trombonist. Day job @artandlogic. Creator of @tmbotg.
@GregClarkMusic I had to look it up to make sure I remembered right! - 1 day ago
Brett g Porter
Brett g Porter

Latest posts by Brett g Porter (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.