Introducing EasyTweeter


Welcome another entry, and another category to the projects page: EasyTweeter!

EasyTweeter is a really simple, one-file python library which makes writing really simple twitter bots easier. It's more or less a wrapper around the existing Tweepy library, which does, to be fair, make the whole process of writing simple twitter bots pretty easy, but there's room for improvement there for a certain use case.

The intended use case is a pretty common one: a bot that just posts some tweets on a schedule. It doesn't interact with people who interact with it, and it doesn't post frequently enough that you need to have a persistent connection open. It starts, generates some text, tweets it, and stops. There's plenty of existing ways to make this happen, but I found they all lacked one pretty critical piece of the puzzle for me: if anyone interacted with the bot, like by retweeting, liking, replying, etc, I wanted to know about it.

I honestly already find writing twitter bots pretty fun, and I've only written one, but they've got a pretty good ratio of effort to reward. Throw some text generation stuff at twitter in a little script, throw that on a schedule so you don't have to remember to run it, and it'll just keep chugging along, occasionally coming up with mildly good tweets. That's already nice; I found it quite fun just to make text generation commands in a discord bot that my friends and I use, but when it's on twitter, it's a public thing. It's even nicer to have someone you've never met follow your bot, but if you ever want to find out about that, you have to actually go look at your bot's account, and who's got time for that? Not me, so I wanted to automate it.

There's one problem...

Twitter's API fucking sucks

Trying to automate it was a massive pain in the ass.

Twitter's API is geared toward doing exactly one thing: running Twitter. If you aren't Twitter, and therefore aren't running Twitter, it doesn't like you. Twitter is so simple, and you'd think the api would be nice and simple too, but boy, it isn't, and in ways you wouldn't think.

An example: the official, Twitter approved way to retrieve the replies to a tweet with their API is to have a persistent connection to the API which reads the bot's home page (the same one you get if you just go to twitter.com), and watch for tweets which are replies to something the bot has tweeted, and store those in a file. Then, if you want to know all the replies you've gotten, load up the tweets from that file.

If what you want is to query for all replies, given a tweet's id/url, that's the best you can do, which is way worse than just using wget on the twitter page for the tweet you want and scraping it.

That's insane.

The twitter website can do that no problem, I guess because they actually do all that, just so you can get all the replies when you look at a tweet, but if you're an API consumer, you can't. I mean, there's no API call that just does that. I guess you could setup something to do that, but would you? Should you? Is it worth it?

Taking one for the team

No. It's not worth it.

You should just pull up the bot's home page and look at all the recent replies you can see (within reason, so you don't hit the request rate limit - which twitter has really low, by the way) and call it good. You may miss some replies if you don't check that frequently. If you just automate checking, like, say, with a library, you're unlikely to miss something. It's possible, but as someone playing an engineer on TV, you've got to make the cost/benefit analysis about whether it's worth the effort required to make the code do the official thing.

It's not.

Let's not forget though, our goal is to notify our owner if we get interacted with. We've got favorites, retweets, new followers, and direct messages to do too. Those aren't as bad as replies, but figuring that out is a fair amount of work. Once you've got it working though, it's nice to get notified when someone interacts with your silly bot. If you play an engineer on TV really well, you can reuse that with future bots, which I do plan to make. And think about it, how many people want to do this? I'll bet probably a fair number. If our code makes it easy, you can tweet with reckless abandon without thinking about the disaster that is Twitter's API.

An Example

I think I've both made the code work and made it easy. That's a claim, and let's do a quick demo of the library to back it up.

As an example, let's make a recreation of a well known bot: the fuck every word bot. It's a bot that just says fuck before every word in the dictionary. It's about as simple as you can get, but it gets you some pretty mildly humorous tweets, and just imagine, it has to be simple to program. Even still, I don't know how big their code is, but I'll bet I can make it smaller.

Before we start, let's break down the problem.

  1. Authenticate with twitter.
  2. Generate the text to tweet.
  3. Tweet it.
  4. Check for interactions.

With some setup, the library automates numbers 1, 3 and 4, so the entire program, minus 2, becomes this:

from EasyTweeter import EasyTweeter

bot = EasyTweeter('fuck_every_word_redeux')
bot.tweet('insert some text here')
bot.checkForUpdates()

That 'fuck_every_word_redeux' in there is the directory name where the bot stores its logs, configs and state. Speaking of, let's do some setup.

All you need to do is make that directory, and a file in that directory named credentials.ini, and put this in it:

[TwitterCredentials]
ConsumerKey = [put your consumer key here]
ConsumerSecret = [put your consumer secret here]
AccessToken = [put your access token here]
AccessTokenSecret = [put your access token secret here]

and fill in those fields with the appropriate stuff from Twitter's developer site.

Setup done.

Want to put the directory somewhere else, other than next to your bot's .py file? Pass the full path to the EasyTweeter constructor. Want the logs, state, or credentials file stored elsewhere, outside that directory? You can subclass EasyTweeter and override a method that determines the path.

Alright, let's do text generation. Every time this program runs, we want it to come up with something different: the word fuck, concatenated with a new word out of the dictionary. The way I'm planning on accomplishing this is by storing a file, which will sit next to the bot's .py file, called dictionary.txt, which contains each word in the dictionary, one per line. Getting a file like this is left as an exercise for the reader, but as a hint, most Linux distros come with one, so a copy is only one Google away. Each time the bot runs, it'll pick the first word, tweet it, and remove it from that file, so the next time it runs, it'll tweet the next word. Luckily, we're in Python, so that's pretty easy.

from EasyTweeter import EasyTweeter

bot = EasyTweeter('fuck_every_word_redeux')

with open('dictionary.txt') as dictionary:
    lines = dictionary.readlines()
bot.tweet("fuck " + lines.pop(0))
with open('dictionary.txt', 'w') as dictionary:
    dictionary.writelines(lines)

bot.checkForUpdates()

A criticism could be made that this is awfully computationally expensive for as small a bot as this, since we have to read the whole file in, cut one item out of it, and then write the whole file out again, and that's a fair point, but a counter point may be that the example dictionary file I have is only 433kb, and the bot we're mimicking only runs every half hour. If you do want to resolve that, you could have the file sorted in reverse, and delete the last line instead, with an approach like this, but I'll leave this point be in the name of brevity (which, after devoting a paragraph to this, is making me suspect I'm not very good at it).

One thing you might want is better logging. All the methods of bot have exception handling in them, so if anything happens in there, it'll be logged so you can fix it, but if something were to blow up in the code we added, it won't. Let's address that.

from EasyTweeter import EasyTweeter

bot = EasyTweeter('fuck_every_word_redeux')

try:
    with open('dictionary.txt') as dictionary:
        lines = dictionary.readlines()
    bot.tweet("fuck " + lines.pop(0))
    with open('dictionary.txt', 'w') as dictionary:
        dictionary.writelines(lines)
except Exception as e:
    bot.logger.exception('Exception caused bot to fail.')

bot.checkForUpdates()

A breakdown:

  • We tell the bot about that directory we made by passing the directory name into the constructor of EasyTweeter. In addition to storing the credentials.ini file, the bot keeps some state in there, such as the favorites, retweets, etc which it knows about, as well as the logs.
  • We load up the lines in the dictionary file into a list.
  • We tweet out the first word in the list, removing it from the list in the process. When tweeting, this is also when authentication to Twitter occurs.
  • We write out the list of words, now without the first entry, back into the file.
  • If any errors happen in our "text generation" (which is likely more complicated in your real use) they'll show up in our bot's logs, giving us valuable debugging information, since, because this is running on a schedule, we likely won't know it has failed.
  • We check for updates on our account. If anyone has interacted with the bot since the last time it's run, via following it, favoriting, retweeting, or replying to any of it's tweets, or direct messaging it, you'll see it in the logs on a line with [NEW].

Hopefully you'll agree that's a pretty good ratio of code you care about to boilerplate.

And let's remember what's not here:

  • No hard-coded credentials in your code.
  • No (visible) authentication code.
  • No (visible) code to keep track of what favorites, retweets, replies, etc we've seen already.
  • No (visible) code to deal with rate limiting.

Now go forth and tweet easily

So that's pretty cool, huh?

It's not much, it's one file after all, and I would say this as the author of the thing, but when it's that easy, it makes me interested in what I can make with it.

In fact, new bots sound like pretty fun projects, or maybe renovating an old one...

I'd watch this spot if I was you.