# Is This Loss? Part 1: Building a Discord Chatbot

### 2018-06-15 dev web python chatbot

So my schedule has been significantly emptier recently, it’s the weekend and I’m looking for a project. I thought about what I could do and came across chatbots, they’re fun to play with and not as expensive as a website. Interaction will be through basic commands, nothing fancy like natural language processing. However I will be throwing in some TensorFlow because this is going to be an objection recognition bot.

With this technology at our disposal, we must do something important, this project must matter and make a positive difference to the world. So I’ve decided that the noble cause which this bot will serve is going to be detecting loss memes. I’m writing this as I go so this series may not go smoothly or may even not be complete at all but writing this gets me going.

# Registering the Discord bot

First step is to register an app on Discord, the developer portal has the option “My Apps” on the top of the left panel. I’m naming it “Is This Loss?", keeping it simple so no description or picture for now. After creating the app, there’s an option at the bottom to turn this into a bot user, which is what I want. After converting to a bot user, the bot token can be obtained.

## Adding the bot to a server

To test the bot, I’ll need a Discord server and a dummy channel where I have permissions to add and spam bots. The my apps page generously provides a Generate OAuth2 URL button, click and allow permissions to “Send Messages” and “Attach Files”. Navigating to the generated URL adds the bot to your server.

# Starting the project

I like to create Python project skeletons with pyscaffold. After installing it, the skeleton can be generated with.

$putup Is-This-Loss$ cd Is-This-Loss


Next I’m going to create a virtual environment and download the dependencies for this part which is disco, pillow, and requests. For now I’m keeping this simple with the standard version that excludes optional dependencies, see here.

$python3 -m venv .$ source bin/activate
$pip install -U disco-py$ pip install -U pillow


Also, remember to add an appropriate gitignore so git doesn’t include all the unnecessary crap.

$curl https://www.gitignore.io/api/venv,python -o .gitignore  ## Bot command I setup my src folder like so, disco requires the bot token in config.json. src/ is_this_loss/ plugins/ __init__.py object_detector.py config.json  Here’s a template for config.json from the docs.  { "token": "YOUR_BOT_TOKEN", "bot": { "plugins": [ "plugins.object_detector" ] } }  Next in object_detector.py a Plugin class is needed to handle commands. In my case the bot will support only one command which is !loss <link> The bot will listen for that command, go to the link and download a picture from it. It will run an inference through TensorFlow to detect loss memes in it. It then draws bounding boxes and sends back the new image as an attachment. For now it will only download and send back the picture at the link. First the class needs a method to handle the command event, specify it with @Plugin.command. from disco.bot import Plugin from io import BytesIO from PIL import Image import requests class ObjectDetector(Plugin): @Plugin.command('!loss', '<link:str...>') def command_detect_loss(self, event, link): pass  It will also need a method to load the link and return it as a file, here BytesIO is used so there’s no need to save a file. I don’t want to support too many formats, just jpgs and png, throwing an OSError if that’s not the case since it’s is the same error that Image.open throws when an invalid image is loaded. format_map = { 'JPEG': 'jpg', 'PNG': 'png', 'JPEG 2000': 'jpg' } def load_image_from_url(self, url): response = requests.get(url) file = BytesIO(response.content) image = Image.open(file) if image.format not in self.format_map.keys(): raise OSError('Unrecognized format') return image  ## Retrieving and echoing the image Next, a method to create a file from the image returned is needed for later when I add the inferences code. file.seek(0) got me stuck for a while, I was receiving attachments of size 0 until I realized that BytesIO is a stream implementation. The “cursor” needs to seek to the start in order to send the contents. def create_attachment_from_image(self, image): file = BytesIO() image.save(file, 'PNG') file.seek(0) return file  Finally back in the handler everything can be tied together. @Plugin.command('!loss', '<link:str...>') def command_detect_loss(self, event, link): try: image = self.load_image_from_url(link) except OSError as e: event.msg.reply("I can't find a JPEG or PNG image at the link you've given") return file = self.create_attachment_from_image(image) filename = 'loss_inference.png' event.msg.reply( "Hi I found the following image at the link you've given.", attachments=[(filename, file)])  So now the bot has the minimal components to run and do something. Here it is in full. object_detector.py from disco.bot import Plugin from io import BytesIO from PIL import Image import requests class ObjectDetector(Plugin): format_map = { 'JPEG': 'jpg', 'PNG': 'png', 'JPEG 2000': 'jpg' } def load_image_from_url(self, url): response = requests.get(url) file = BytesIO(response.content) image = Image.open(file) if image.format not in self.format_map.keys(): raise OSError('Unrecognized format') return image def create_attachment_from_image(self, image): file = BytesIO() image.save(file, 'PNG') file.seek(0) return file @Plugin.command('!loss', '<link:str...>') def command_detect_loss(self, event, link): try: image = self.load_image_from_url(link) except OSError as e: event.msg.reply("I can't find a JPEG or PNG image at the link you've given") return file = self.create_attachment_from_image(image) filename = 'loss_inference.png' event.msg.reply( "Hi I found the following image at the link you've given.", attachments=[(filename, file)])  The following command executed in the is_this_loss directory runs the bot. $ python3 -m disco.cli --config config.json
[INFO] 2018-06-16 16:39:12,601 - Bot:485 - Adding plugin module at path "plugins.object_detector"
[INFO] 2018-06-16 16:39:12,621 - HTTPClient:271 - GET https://discordapp.com/api/v7/gateway (None)
[INFO] 2018-06-16 16:39:15,870 - GatewayClient:147 - Opening websocket connection to URL wss://gateway.discord.gg?v=6&encoding=json&compress=zlib-stream
[INFO] 2018-06-16 16:39:17,725 - GatewayClient:212 - WS Opened: sending identify payload
[INFO] 2018-06-16 16:39:17,726 - GatewayClient:122 - Received HELLO, starting heartbeater...


Let’s test the bot in Discord.

Here you can see that Discord recognized an image from the website and displayed it but the bot didn’t as it requires a direct link to the image. This could be a future feature to improve the versatility of the image scraper.

I’m leaving this here for now, in part 2 I plan to tie a general object detector model to this bot with TensorFlow. Then in part 3 I will train the model into a specialized loss recognizer. The repository will be made public on GitHub and GitLab after cleaning out all junk and bot tokens.

Part 2 is available here.