Creating a Bot for Discord

I’ve been playing World of Warcraft lately in my spare time. Something I’ve been enjoying is the Mythic+ dungeons that were introduced with the Legion expansion. For these dungeons players must complete them within a time limit, but doing so requires a very competent team of players. The problem with the in-game tools that World of Warcraft provides is that it doesn’t show all of the data you may need to properly audit a players progress to see if they are experienced enough to do the dungeon.

I wanted to create a way to easily view a player’s progress without having to click through multiple menus on the games player profile website. I decided to create a bot user for the chat program Discord which ideally would present all of the information I needed in an easy to digest format.

You're Not Prepared

I decided to write my bot in Python as there’s an open source API wrapper called discord.py available which does all of the heavy lifting for you when it comes to sending and recieving messages from Discord. There’s also something very similar for NodeJS.

In order to create a connection to Discord you’ll need a Discord API key with a bot user token, and to get information from World of Warcraft you’ll need to sign up for the Blizzard API.

Setting up a simple bot is easy. First you’ll also need to invite your bot user to your Discord server using the following URL, replacing the client ID part first.

https://discordapp.com/oauth2/authorize?&client_id=YOUR_CLIENT_ID_HERE&scope=bot&permissions=0

Now all you need to do is use the following scaffold, and once the script is executed anytime a user types a message in the chat which starts with ‘hello’ your bot will reply with the word ‘world’. This will be known as our message listening function.

import discord

# You'll need to replace abc123 with your own Discord bot token.
DISCORD_BOT_TOKEN = 'abc123'

client = discord.Client()

@client.event
async def on_message(message):
    if message.content.startswith('hello'):
        await client.send_message(message.channel, 'world')
        

client.run(DISCORD_BOT_TOKEN)

Making It Do Things

Now that the bot can post messages to the Discord server some methods are needed so it does more than just say the word ‘world’. I want my bot to respond to a set of specific trigger words so it knows it’s being talked to. For this I decided to go with the format !armory pve <name> <realm>. Once a user sends a message with this format I want it to make an API call to World of Warcraft using the name and realm fields in the message, fetch all of the data I need, and then return that data to my message listening function. From there I could then use that information to format and post the information to Discord.

I created an ‘umbrella’ function called character_info and set it up to take three arguments, the players name, realm, and the type of content requested, for example PVE. The goal of this function is to create and return a piece of JSON with information pertaining to the character.

def character_info(name, realm, query):

    char_name = name
    char_realm = realm

    # Fetches data from the Blizzard API.
    info = get_data(char_name, char_realm, 'items')

The get_data function in the example above is used to do exactly that, get data. The World of Warcraft API doesn’t have a single method for fetching all of the data I want to show, so this reusable function shows up multiple times throughout my bots code. The function takes three arguments, the players name, realm, and the type of data you want to retrieve, for instance ‘items’ or ‘achievements’. These data types can be found on the Blizzard API explorer.

def get_data(name, realm, field):

    # WOW_API_KEY gets stored as a enviroment constant near the top of the file.
    path = 'https://us.api.battle.net/wow/character/%s/%s?fields=%s&locale=en_US&apikey=%s' % (
        realm, name, field, LOCALE, WOW_API_KEY)

    try:
        request = requests.get(path)

        # Make sure the request doesn't error.
        request.raise_for_status()
        request_json = request.json()

    except requests.exceptions.RequestException as error:
        # If there's an issue or a character doesn't exist, return an empty string.
        request_json = ''

    return request_json

Once it has returned with the requested data we can then start forming our character object.

def character_info(name, realm, query):

    char_name = name
    char_realm = realm

    info = get_data(char_name, char_realm, 'items')

        # If the player typed !armory pve <name> <realm> then process it as a PVE request.
        if query == 'pve':
            pve_character_sheet = {
                'name': info['name'],
                'level': info['level'],
                'realm': info['realm'],
                'ilvl': info['items']['averageItemLevelEquipped'],        
            }

            return pve_character_sheet

Now whenever character_info is called it returns a JSON object with some information that your message listening function can use to display in the Discord chat. In the following example the bot will reply with the players item level whenever !armory pve <name> <realm> is typed.

@client.event
async def on_message(message):
    if message.content.startswith('!armory pve'):

        # Simple utility function that splits the string.
        split = split_query(message.content, 'pve')

        # Sends the split string to the character_info function to build a character sheet.
        info = character_info(split[0], split[1], split[2])

        # Posts the message
        await client.send_message(message.channel, info['ilvl'])

Expanding Everything

Now that the basic functionality of the bot has been implemented it’s time to extend its abilities. Because our character_info function simply returns some JSON we can easily add additional properties so it returns even more information about a specific character.

I want a way to check if a player has completed a specific achievement, luckily for me the World of Warcraft API has a method for fetching achievement data about a character, however the way it’s organized can be a bit confusing. Each achievement is represented by an ID number and it’s stored within a lengthy array called achievementsCompleted. I wrote a basic function that accepts data from the achievements API and then sorts through the achievementsCompleted array. If the user hasn’t completed the achievement it returns ‘In Progress’, if they have it switches to ‘Completed’.

def character_achievements(achievement_data):
    achievements = achievement_data['achievements']

    # Defaults to 'In Progress'.
    keystone_master = 'In Progress'
    keystone_conqueror = 'In Progress'
    keystone_challenger = 'In Progress'

    # If the ID is found then switch to 'Completed'.
    if 11162 in achievements['achievementsCompleted']:
        keystone_master = 'Completed'

    if 11185 in achievements['achievementsCompleted']:
        keystone_conqueror = 'Completed'

    if 11184 in achievements['achievementsCompleted']:
        keystone_challenger = 'Completed'

    achievement_list = {
        'keystone_master': keystone_master,
        'keystone_conqueror': keystone_conqueror,
        'keystone_challenger': keystone_challenger
    }

    return achievement_list

In an effort to make the achievement tracking function cleaner I setup a constants.py file which would act as a dictionary, mapping ID numbers to a name.

AC_KEYSTONE_MASTER = 11162
AC_KEYSTONE_CONQUEROR = 11185
AC_KEYSTONE_CHALLENGER = 11184

Within my achievement function I can now see if the achievementsCompleted array contains an ID by simply referncing the name I setup in the constants file.

if AC_KEYSTONE_MASTER in achievements['achievementsCompleted']:
    keystone_master = 'Completed'

Now within character_info I call the achievement_data function and use the JSON it returns to add more information to my character object.

def character_info(name, realm, query):

    char_name = name
    char_realm = realm

    info = get_data(char_name, char_realm, 'items')

    # If the data returned isn't an empty string assume it found a character.
    if info != '':
        # Gathers achievement data from the achievements API.
        achievement_data = get_data(name, realm, 'achievements')

        # Sorts and computes the achievement data.
        achievements = character_achievements(achievement_data)

        if query == 'pve':
            pve_character_sheet = {
                'name': info['name'],
                'level': info['level'],
                'realm': info['realm'],
                'ilvl': info['items']['averageItemLevelEquipped'],  
                'keystone_master': achievements['keystone_master'],
                'keystone_conqueror': achievements['keystone_conqueror'],
                'keystone_challenger': achievements['keystone_challenger']
            }

            return pve_character_sheet

Within the message listner function I can now organize the data retrieved from character_info to publish the data to Discord. You can utilize message attachments with discord.py which allow you to modify how the content looks when it’s posted to Discord.

@client.event
async def on_message(message):
    if message.content.startswith('!armory pve'):

        split = split_query(message.content, 'pve')

        info = character_info(split[0], split[1], split[2])

        # Formats the message and posts it to Discord.
        msg = discord.Embed(
            title="%s" % (info['name']),
            colour=discord.Colour(info['class_colour']),
            url="%s" % (info['armory']),
            description="%s %s %s %s" % (
                info['level'], info['faction'], info['spec'], info['class_type']))

        msg.add_field(
            name="Keystone Achievements",
            value="**`Master(+15)`:** `%s`\n**`Conqueror(+10)`:** `%s` \n**`Challenger(+5)`:** `%s`" % (
                info['keystone_master'], info['keystone_conqueror'],
                info['keystone_challenger']),
                inline=True)

        # Posts the attachment.
        await client.send_message(message.channel, embed=msg)

I spent quite a bit of time extending the functionality of my bot, it can track notable achievements, raid progression, item level, race, faction, and more. I also added another method called !armory pvp <name> <realm> so you can gather information about a characters PVP progression.

Discord WoW Bot

You are Prepared!

If you’d like to check out the code or use my bot you can find it on GitHub. If you feel the bot is missing something feel free to raise an issue or make a pull request!

Hopefully this helps you get started with your own Discord bot. If you have any questons feel free to get in touch on Twitter or via my contact form.