How to set up an obscenely easy, AI-API-using tweetbot in Python
Using Tweepy and some image recognition public APIs to help Twitter users with an all-important question — IsItNSFW?
I love learning on the go. The approach is particularly useful for me this year. 2019 in my case means a professional transition from humanities (focus on translation) to tech (focus on NLP/ML). Here’s one example of how quickly one can apply freshly-acquired skills. Also, this story comes in a series of threes.
A month ago, after a poke from Piotr Migdał, I described how to fool neural networks with seemingly pornographic optical illusions. As a spin-off and to learn a new skill, I created a simple tweetbot. @IsItNSFW, upon receiving an image, will respond with the scores a handful of AI image detection models give it.
To create it, I needed to put together three elements:
- Twitter interaction via Tweepy,
- connecting to AI detector APIs,
- and somewhere to host my script.
Twitter interaction via Tweepy
For the first module, Tweepy was my choice after Googling for accessible Twitter-wise Python libraries. Again, there are three Tweepy-related parts of the code:
- linking a Twitter account to the script,
- defining a function that will reply to tweets,
- setting a stream
Prerequisite: Twitter account setup
I started by creating an account my bot will be using on Twitter. Then I headed to the developer dashboard in order to create a new app and obtain the API key, API secret key, Access token and Access token secret.
I noted them down in a safe place. In fact, I stored them straight away in a separate file called secrets.py. That way I can share the rest of the code on GitHub without needing to worry about the secrecy of my API access keys:
#Twitter APIconsumer_key = '...'consumer_secret = '...'access_token = '...'access_secret = '...'
Connecting to Twitter
Only now do I start to write the main script (nsfw_bot.py in my case). I used the newly-created credentials to connect to Twitter using Tweepy’s OAuthHandler class:
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)auth.set_access_token(access_token, access_secret)api = tweepy.API(auth)
The api variable will be the entry point for the operations I will perform with Twitter — in this case, posting replies with the image scores. Tweepy’s API class provides access to the entire Twitter RESTful API methods (see API Reference for more information).
Then I set the tweet_image_ratings() function exactly for that. The function accepts — you guessed it — 3 arguments:
(pic_url: [the tweeted image source],
username: [the username of the user that tweeted the image],
status_id: [the id of the tweet])
My function consults the image with all the three image detectors and replies to the tweets with the corresponding scores and a retweet of the image. In order to retweet, I need to temporarily store the image as temp.jpg:
def tweet_image_ratings(pic_url, username, status_id):
# Take the pic url and tweet the various NSFW ratings
clarifai_score = check_clarifai(pic_url)
deepai_score = check_deepai(pic_url)
sightengine_score = check_sightengine(pic_url)
filename = 'temp.jpg'
request = requests.get(pic_url, stream=True)
if request.status_code == 200:
with open(filename, 'wb') as image:
for chunk in request:
api.update_with_media(filename, status='Is it NSFW, @'+username+'?\n' + clarifai_score+deepai_score+sightengine_score, in_reply_to_status_id=status_id)
The last Tweepy-related bit to fill in — tweet_image_ratings() arguments, that is, the original tweet data. How to get those?
In order to keep the connection open and be able to respond to all incoming and upcoming tweets, I need the streaming API. Tweepy makes it easier to use the Twitter streaming API by hiding authentication, session handling and reading incoming messages under the hood.
I used the streaming API in two steps.
- I created a class inheriting from StreamListener, overriding the on_status method. BotStreamer captures the username and tweet id. If an image is accompanying the tweet, it passes the three arguments the tweet_image_ratings() function described above:
# Called when a new status arrives which is passed down from the on_data method of the StreamListener
def on_status(self, status):
username = status.user.screen_name
status_id = status.id
if 'media' in status.entities:
for image in status.entities['media']:
tweet_image_ratings(image['media_url'], username, status_id)
- Using that class, I instantiated a Stream object and connected to the Twitter API using it:
Connecting to AI detector APIs
The last part of nsfw_bot.py I wrote concerns connecting to the nudity detectors and retrieving their scoring for a given picture. It may surprise you, but I chose three:
For each of these, I had to create an account and an API key, immediately stored in secrets.py. In all three cases, the pricing of the service includes a free tier that is absolutely sufficient for my light use.
client = SightengineClient(sightengine_user, sightengine_secret)
output = client.check('nudity').set_url(pic_url)
score = 'Sightengine: ' + \
score = ''
In all 3 cases, I included a try/except clause for any error handling. This way the bot will only tweet about the successfully retrieved scores.
And that’s it! We’re done with a simple bot. Time to test it…
Check out the complete code of nsfw_bot.py in this GitHub repo.
Where to host my script?
That was the last issue I had to solve. Running it from my computer was not a sustainable option. Literally the first service I stumbled upon was PythonAnywhere. Straightforward, easy to apply with a fairly generous free use tier and helpful staff — it won my heart from the start.
I simply uploaded both nsfw_bot.py and secrets.py to PythonAnywhere. The service comes with a full Python environment installed, so the only thing left to do was open a Bash console in PyAnywhere and run my script.
I was still concerned about the infallibility of the bot out in the wild — will it crash on some unavoidable or unpredictable connection error? Googling for a solution, I found the following trick:
from subprocess import Popen
import datetimefilename = sys.argv
print('\n'+str(datetime.datetime.today())+"\nStarting " + filename)
p = Popen("python " + filename, shell=True)
The miniscript I called forever.py opens the [filename] as a new subprocess every time it produces an error. I added a print(datetime.datetime.today()) statement to have a log on the frequency the script crashes.
The main purpose of this story is to show how little fuss is needed to set up a minimal working tweetbot connecting to several APIs. I was writing it in a public cafe, and realized a limitation of the @IsItNSFW bot itself — after all, many people may be too embarrassed to upload a (verging on a) kinky photo on their Twitter and have it publicly retweeted.
My thanks go to Piotr Migdał for the initial prod and invaluable feedback.