Making a Reddit Bot That Conjures Spirits

I use Reddit a lot, mainly for discussions, news, cute pictures, cool GIFs, and memes. In the midst of all the subreddits, one in particular, r/AskOuija, is this interesting crossroads of the Internet’s ability to collaborate along with its collective humor. But there’s a problem – on Reddit I can easily save and share images and links I find on the platform, but since r/AskOuija is mainly text, is there a way for me to convert text on Reddit into images (and give me an excuse to learn how to use various web APIs)? This is how I learned to make a Reddit bot.

There’s a specific subreddit I like, r/AskOuija. The concept is pretty simple – a user poses a question to the spirits, and, one letter at a time in the comments, the spirits answer, usually with interesting results. The trouble with AskOuija’s format versus other subreddits is that there’s no way I can really save the results unless I screenshot it, but even then, that’s a tad unattractive.

BOOOO I SAY
Boooooo! (yes, I’m a bit nitpicky)

So, let’s remedy this with code!

Here’s the idea – when my bot is mentioned (basically being tagged by a user) in the comments of an r/AskOuija post, the bot would use the Reddit API to grab the submission’s title (the question posed to the spirits) and the flair text (what the spirits responded with, the part that says “Ouija says:”), generate an image with these bits of information, upload the image to Imgur using the Imgur API, and reply back to the initial comment with a link to the newly uploaded image.

I’ll break down the process section by section, much like how I built the bot, starting with necessary libraries. Reddit and Imgur both have APIs to develop with. However, since I didn’t want to dive headfirst with dealing with the APIs, I used two Python wrappers – PRAW and PyImgur. To generate the image, I used Pillow, an imaging library.

API Registration and You

If you’re unfamiliar with the process of using APIs in your code, you’ll need to register your application to receive a client ID and client key to identify your application. With Reddit, after logging in, go to Preferences, then Apps, then scroll down to the “are you a developer? create an app…” button.

Simple Reddit bots like mine are scripts. For the time being, I only put in placeholder URLs in the about and redirect sections. Once you create the app, you should get both a personal use script ID and secret key.

Meanwhile, on Imgur, after logging in, you’ll need to follow a similar process and register an app.

Since this was an application only used by one account as far as Imgur was concerned, I stuck to using anonymous usage. But once you’ve filled the necessary information, and you go to your Settings, then Applications sections, you’ll find the credentials you’ll need to add.

Let’s actually dive into the code. PRAW and PyImgur generate instances that allow your code to interact with Reddit and Imgur. But these instances require the client ID and secret keys generated earlier. PRAW also needs a user agent (in my case, this is the bot’s name and version) and the username and password of the Reddit account associated with the bot. PyImgur only needs the client ID of the Imgur application. If you plan on showing off your source code, it’d be wise to keep this information in a separate file and import it in the source, not committing this separate file. In my case, I made a file called config.py, where I kept all the necessary IDs, secrets, and other identifying information, importing it into the main source code via import config, as shown:

from PIL import Image, ImageDraw, ImageFont
import pyimgur
import praw
import config

im = pyimgur.Imgur(config.IMGUR_ID)

bot = praw.Reddit(user_agent='OuijaMediumBot v0.1',
                  client_id=config.REDDIT_ID,
                  client_secret=config.REDDIT_SECRET,
                  username=config.REDDIT_USER,
                  password=config.REDDIT_PASS)

With this, the code is set to interact with Imgur and Reddit. But I’ll touch on that in a bit. Let’s start someplace simpler, imaging.

Take a Chill PIL

from PIL import Image, ImageDraw, ImageFont

grey = '#5C5D5C'
white = '#FFFFFF'
black = '#000000'
green = '#A2F057'

serif = ImageFont.truetype('serif.ttf', 50)
sans = ImageFont.truetype('sans.ttf', 50)
small_serif = ImageFont.truetype('serif.ttf', 20)

post_text = 'What image will we generate today?'
response_text = 'Ouija says: THISONE'
post_link = 'there would be a link here, but hey, this is only a demonstration'
sizes = []
sizes.append(serif.getsize(post_text)[0])
sizes.append(sans.getsize(response_text)[0])
sizes.append(small_serif.getsize(post_link)[0])
img = Image.new('RGB', (max(sizes) + 100, 240), color=black)
d = ImageDraw.Draw(img)
d.text((50, 50), post_text, fill=white, font=serif)
d.text((50, 110), response_text, fill=green, font=sans)
d.text((50, 170), post_link, fill=grey, font=small_serif)

img.save('hi.png')

This is the heart of the bot – conjuring images (after all, this is an Ouija medium). PIL is pretty simple to use. In essence, all I’ve done was initialize fonts (8-10), figure our horizontal boundaries (15-18), create a new image (19-20), drew the text (21-23), and then saved the image (25).  Let’s start with the fonts. PIL has a module called ImageFont that allows it to use fonts and draw them. I used the DejaVu font family, but you’re free to use any bitmap, TrueType, or OpenType fonts. To use a TrueType font, use the truetype function, providing the path to the font you want to use (in my case, I copied the font files into the same directory as the source script), and the size (measured in points). I need to generate the fonts so I can figure out the size the post text would be, so I can generate an appropriately sized image. You can see me do this in lines 15-18. Image.new creates an image, taking a mode argument (this defines the colorspace and pixel depth, i.e. 8-bit RGB(A) or 8-bit HSV), a 2-tuple for size (width, height) in pixels, and a color the image should use (provided either as a supported word, a 3-tuple, or a hex string). ImageFonts have a method getsize that returns a 2-tuple containing the width and height the provided string would be in pixels. You see me use the first part of the tuple (the width) so I generate an image that would fit the text with 100 pixels extra (for 50 pixel margins on the left and right). Once an ImageDraw module is created, it takes the Image object you provide and provides a drawing interface for it. Once you have this ImageDraw object, you can write text with the text method. The arguments it takes, from left to right, are the position (a 2-tuple representing the x and y pixel coordinates the top left corner of the text should be drawn from, with respect to the top left corner of the image), the text to be drawn, the fill color to use, and the ImageFont to use. Once you’ve made all your various draw calls, it’s time to save, using the save function. Later on, we’ll use PRAW to grab the post text and response text.

I won’t disclose how long it took me to get the colors, the fonts, the positions, and the image size just right because it’s pretty embarrassing.

From Image to Imgur

from PIL import Image, ImageDraw, ImageFont
import pyimgur
import config

im = pyimgur.Imgur(config.IMGUR_ID)

grey = '#5C5D5C'
white = '#FFFFFF'
black = '#000000'
green = '#A2F057'

serif = ImageFont.truetype('serif.ttf', 50)
sans = ImageFont.truetype('sans.ttf', 50)
small_serif = ImageFont.truetype('serif.ttf', 20)

post_text = 'Where will this image end up?'
response_text = 'Ouija says: RIGHTHERE'
post_link = 'there would be a link here, but hey, this is only a demonstration'
sizes = []
sizes.append(serif.getsize(post_text)[0])
sizes.append(sans.getsize(response_text)[0])
sizes.append(small_serif.getsize(post_link)[0])
img = Image.new('RGB', (max(sizes) + 100, 240), color=black)
d = ImageDraw.Draw(img)
d.text((50, 50), post_text, fill=white, font=serif)
d.text((50, 110), response_text, fill=green, font=sans)
d.text((50, 170), post_link, fill=grey, font=small_serif)

img.save('hi.png')

title = 'API TEST {}'.format(post_text)
uploaded_image = im.upload_image('hi.png', title=title, description=post_link)

print(uploaded_image.link)

Getting the newly made image to Imgur is simple. Use PyImgur’s upload_image function (27), feeding the file path of the image, title text, and description text. In my case, I intend to make the title and description texts the URL of the submission. To test the functionality, I just printed it to the console, and verified it uploaded by going to the URL myself.

But, just to be sure, you could also check it yourself by clicking right here.

From Imgur to Reddit

So, we have a way to generate and post images, but now, let’s get the bot to respond to commenters on Reddit.

from PIL import Image, ImageDraw, ImageFont
import pyimgur
import config

im = pyimgur.Imgur(config.IMGUR_ID)

bot = praw.Reddit(user_agent='OuijaMediumBot v0.1',
                  client_id=config.REDDIT_ID,
                  client_secret=config.REDDIT_SECRET,
                  username=config.REDDIT_USER,
                  password=config.REDDIT_PASS)

grey = '#5C5D5C'
white = '#FFFFFF'
black = '#000000'
green = '#A2F057'
serif = ImageFont.truetype('serif.ttf', 50)
sans = ImageFont.truetype('sans.ttf', 50)
small_serif = ImageFont.truetype('serif.ttf', 20)

subreddit = bot.subreddit('test')

for comment in subreddit.stream.comments():
    if 'u/ouijamediumbot' in comment.body.lower():
        submission = comment.submission
        post_text = bot.submission(id=submission).title
        response_text = bot.submission(id=submission).link_flair_text
        post_link = 'www.reddit.com/r/AskOuija/comments/{}'.format(submission)
        sizes = []
        sizes.append(serif.getsize(post_text)[0])
        sizes.append(sans.getsize(response_text)[0])
        sizes.append(small_serif.getsize(post_link)[0])
        img = Image.new('RGB', (max(sizes) + 100, 240), color=black)
        d = ImageDraw.Draw(img)
        d.text((50, 50), post_text, fill=white, font=serif)
        d.text((50, 110), response_text, fill=green, font=sans)
        d.text((50, 170), post_link, fill=grey, font=small_serif)

        img.save('hi.png')

        title = 'TEST - AskOuija {}'.format(post_text)
        uploaded_image = im.upload_image('hi.png', title=title, description=post_link)

        print(uploaded_image.link)
        comment.reply('[I have conjured the spirits for you]({})'.format(link))
        print('Completed\n')

On line 23, we can set the bot’s subreddit to monitor and then get a stream of all the comments posted onto that subreddit. Though its final destination would be r/AskOuija, I pointed it over to r/test, a common testing ground. The for-loop goes through each comment in the stream and checks if any contain a mention of the bot. If there is, first, it grabs the submission ID the comment comes from. It uses this ID to get the title and flair text from the submission, or, the question and response. It also generates the URL the submission comes from. Then, the rest should be familiar – it generates the image, draws the text, saves the image, and uploads it to Imgur. The only difference now is that it takes the link generated after uploading and replies to the comment that summoned it with a link to the newly uploaded image. You might have noticed the formatting the bot is replying with, but, to those not familiar with Reddit’s comment markdown, this is the formatting for hyperlinks.

And, we’re done! The code needs to be hosted and running somewhere, though, code like this probably wouldn’t make much of an impact being ran by, say, the spare laptop I have for my TV. It’s not entirely perfect though – probably one of the big problems it can run into is exceptions (for example, reaching the limits of my API access). This would abort the code, meaning I’d need to monitor it and rerun it if that happened. Down the line, I’ll have try-except blocks to catch exceptions to increase the bot’s longevity.

A Word on Botiquette

Ok, maybe you made a cool Reddit bot, but it still needs to follow some rules. First, you need to follow the terms of the API. Also, your bot needs to have some manners. Your bot is still a Reddit user, so, it’ll also have to follow rules subreddits set, or, it’ll be on the fast track to a ban list. If you’re making a new account for your bot, and it doesn’t have a lot of karma or age, its posting ability will be a bit hampered as well. I bring this up because you might be wondering why I haven’t deployed my bot on r/AskOuija yet. Well…

I’ll be messaging the moderators to see if I can test my bot on the actual subreddit. I also need to let my bot gain a little age and experience before letting it go live its bot life. But until then…