DEV Community

RitzaCo for Ritza

Posted on • Originally published at docs.replit.com

Discord role assignment bot with Python

Discord is a free-to-use chat server application that was initially developed for gamers but is becoming increasingly widely used by many different communities. Anyone can use it to create a chat server for discussion over text as well as voice and video. In addition to hosting human members, these servers can also host special automated users, called bots, which are capable of a variety of fun and useful tasks: everything from playing music to helping human moderators.

In this tutorial, we'll create a welcome bot for our programming discussion Discord server. This bot will welcome users as they join and assign them roles and private channels based on their stated interests. By the end of this tutorial, you will:

  • Have familiarity with the process of creating a Discord bot application.
  • Be able to use discord.py to develop useful bot logic.
  • Know how to host Discord bots on Replit!

Getting started

Sign in to Replit or create an account if you haven't already. Once logged in, create a Python repl.

Creating a new repl

Creating a Discord application

Open another browser tab and visit the Discord Developer Portal. Log in with your Discord account, or create one if you haven't already. Keep your repl open – we'll return to it soon.

Once you're logged in, create a new application. Give it a name, like "Welcomer".

Creating a new Discord application

Discord applications can interact with Discord in several different ways, not all of which require bots, so creating one is optional. That said, we'll need one for this project. Let's create a bot.

  1. Click on Bot in the menu on the left-hand side of the page.
  2. Click Add Bot.
  3. Give your bot a username (such as "WelcomeBot").
  4. Click Reset Token and then Yes, do it!
  5. Copy the token that appears just under your bot's username.

Creating a Discord bot

The token you just copied is required for the code in our repl to interface with Discord's API. Return to your repl and open the Secrets tab in the left sidebar. Create a new secret with DISCORD_TOKEN as its key and the token you copied as its value.

src="https://replit-docs-images.bardia.repl.co/images/tutorials/46-discord-role-bot/secret-token.png"
alt="Secret token"
/>

Once, you've done that, return to the Discord developer panel. We need to finish setting up our bot.

First, disable the Public Bot option – the functionality we're building for this bot will be highly specific to our server, so we don't want anyone else to try to add it to their server. What's more, bots on 100 or more servers have to go through a special verification and approval process, and we don't want to worry about that.

Second, we need to configure access to privileged Gateway Intents. Depending on a bot's functionality, it will require access to different events and sources of data. Events involving users' actions and the content of their messages are considered more sensitive and need to be explicitly enabled.

For this bot to work, we'll need to be able to see when users join our server, and we'll need to see the contents of their messages. For the former, we'll need the Server Members Intent and for the latter, we'll need the Message Content Intent. Toggle both of these to the "on" position. Save changes when prompted.

Bot intents

Now that we've created our application and its bot, we need to add it to a server. We'll walk you through creating a test server for this tutorial, but you can also use any server you've created in the past, as long as the other members won't get too annoyed about it becoming a bot testing ground. You can't use a server that you're just a normal user on, as adding bots requires special privileges.

Open Discord.com in your browser. You should already be logged in. Then click on the + icon in the leftmost panel to create a new server. Alternatively, open an existing server you own.

New server

In a separate tab, return to the Discord Dev Portal and open your application. Follow these steps to add your bot to your server:

  1. Click on OAuth2 in the left sidebar.
  2. In the menu that appears under OAuth2, select URL Generator.
  3. Under Scopes, mark the checkbox labelled bot.
  4. Under Bot Permissions, mark the checkbox labelled Administrator.
    Bot permissions

  5. Scroll down and copy the URL under Generated URL.
    Generated url

  6. Paste the URL in your browser's navigation bar and hit Enter.

  7. On the page that appears, select your server from the drop-down box and click Continue.

  8. When prompted about permissions, click Authorize, and complete the CAPTCHA.
    Bot connect

  9. Return to your Discord server. You should see that your bot has just joined.

Now that we've done the preparatory work, it's time to write some code. Return to your repl for the next section.

Writing the Discord bot code

We'll be using discord.py to interface with Discord's API using Python. Add the following code scaffold to main.py in your repl:

import os, re, discord
from discord.ext import commands

DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")

bot = commands.Bot(command_prefix="!")

@bot.event
async def on_ready():
    print(f"{bot.user} has connected to Discord!")

bot.run(DISCORD_TOKEN)
Enter fullscreen mode Exit fullscreen mode

First, we import the Python libraries we'll need, including discord.py and its commands extension. Next we retrieve the value of the DISCORD_TOKEN environment variable, which we set in our repl's secrets tab above. Then we instantiate a Bot object. We'll use this object to listen for Discord events and respond to them.

The first event we're interested in is on_ready(), which will trigger when our bot logs onto Discord (the @bot.event decorator ensures this). All this event will do is print a message to our repl's console, telling us that the bot has connected.

Note that we've prepended async to the function definition – this makes our on_ready() function into a coroutine. Coroutines are largely similar to functions, but may not execute immediately, and must be invoked with the await keyword. Using coroutines makes our program asynchronous, which means it can continue executing code while waiting for the results of a long-running function, usually one that depends on input or output. If you've used JavaScript before, you'll recognize this style of programming.

The final line in our file starts the bot, providing DISCORD_TOKEN to authenticate it. Run your repl now to see it in action. Once it's started, return to your Discord server. You should see that your bot user is now online.

Online bot

Creating server roles

Before we write our bot's main logic, we need to create some roles for it to assign. Our Discord server is for programming discussion, so we'll create roles for a few different programming languages: Python, JavaScript, Rust, Go, and C++. For the sake of simplicity, we'll use all-lowercase for our role names. Feel free to add other languages.

You can add roles by doing the following:

  1. Right-click on your server's icon in the leftmost panel.
  2. From the menu that appears, select Server Settings, and then Roles.
  3. Click Create Role.
    Create role

  4. Enter a role name (for example, "python") and choose a color.

  5. Click Back.

  6. Repeat steps 3–5 until all the roles are created.

Your role list should now look something like this:

Roles list

The order in which roles are listed is the role hierarchy. Users who have permission to manage roles will only be able to manage roles lower than their highest role on this list. Ensure that the WelcomeBot role is at the top, or it won't be able to assign users to any of the other roles, even with Administrator privileges.

At present, all these roles will do is change the color of users' names and the list they appear in on the right sidebar. To make them a bit more meaningful, we can create some private channels. Only users with a given role will be able to use these channels.

To add private channels for your server's roles, do the following:

  1. Click on the + next to Text Channels.
  2. Type a channel name (e.g. "python") under Channel Name.
  3. Enable the Private Channel toggle.
  4. Click Create Channel.
  5. Select the role that matches your channel's name.
  6. Repeat for all roles.

Create channel

As the server owner, you'll be able to see these channels regardless of your assigned roles, but normal members will not.

Messaging users

Now that our roles are configured, let's write some bot logic. We'll start with a function to DM users with a welcome message. Return to your repl and enter the following code just below the line where you defined bot:

async def dm_about_roles(member):
    print(f"DMing {member.name}...")

    await member.send(
        f"""Hi {member.name}, welcome to {member.guild.name}! 

Which of these languages do you use:

* Python (🐍)
* JavaScript (πŸ•ΈοΈ)
* Rust (πŸ¦€)
* Go (🐹)
* C++ (πŸ‰)

Reply to this message with one or more of the language names or emojis above so I can assign you the right roles on our server.
"""
    )
Enter fullscreen mode Exit fullscreen mode

This simple function takes a member object and sends it a private message. Note the use of await when running the coroutine member.send().

We need to run this function when one of two things happens: a new member joins the server, or an existing member types the command !roles in a channel. The second one will allow us to test the bot without constantly leaving and rejoining the server, and let users change their minds about what programming languages they want to discuss.

To handle the first event, add this code below the definition of on_ready:

@bot.event
async def on_member_join(member):
    await dm_about_roles(member)
Enter fullscreen mode Exit fullscreen mode

The on_member_join() callback supplies a member object we can use to call dm_about_roles().

For the second event, we'll need a bit more code. While we could use discord.py's bot commands framework to handle our !roles command, we will also need to deal with general message content later on, and doing both in different functions doesn't work well. So instead, we'll put everything to do with message contents in a single on_message() event. If our bot were just responding to commands, using @bot.command handlers would be preferable.

Add the following code below the definition of on_member_join():

@bot.event
async def on_message(message):
    print("Saw a message...")

    if message.author == bot.user:
        return # prevent responding to self

    # Respond to commands
    if message.content.startswith("!roles"):
        await dm_about_roles(message.author)
Enter fullscreen mode Exit fullscreen mode

First, we print a message to the repl console to note that we've seen a message. We then check if the message's author is the bot itself. If it is, we terminate the function, to avoid infinite loops. Following that, we check if the message's content starts with !roles, and if so we invoke dm_amount_roles(), passing in the message's author.

Stop and rerun your repl now. If you receive a CloudFlare error, type kill 1 in your repl's shell and try again. Once your repl's running, return to your Discord server and type "!roles" into the general chat. You should receive a DM from your bot.

Bot direct message

Assigning roles from replies

Our bot can DM users, but it won't do anything when users reply to it. Before we can add that logic, we need to implement a small hack to allow our bot to take actions on our server based on the contents of direct messages.

The Discord bot framework is designed with the assumption that bots are generic and will be added to many different servers. Bots do not have a home server, and there's no easy way for them to trace a process flow that moves from a server to private messages like the one we're building here. Therefore, our bot won't automatically know which server to use for role assignment when that user replies to its DM.

We could work out which server to use through the user's mutual_guilds property, but it is not always reliable due to caching. Note that Discord servers were previously known as "guilds" and this terminology persists in areas of the API.

As we don't plan to add this bot to more than one server at a time, we'll solve the problem by hardcoding the server ID in our bot logic. But first, we need to retrieve our server's ID. The easiest way to do this is to add another command to our bot's vocabulary. Expand the if statement at the bottom of on_message() to include the following elif:

    elif message.content.startswith("!serverid"):
        await message.channel.send(message.channel.guild.id)
Enter fullscreen mode Exit fullscreen mode

Rerun your repl and return to your Discord server. Type "!serverid" into the chat, and you should get a reply from your bot containing a long string of digits. Copy that string to your clipboard.

Go to the top of main.py. Underneath DISCORD_TOKEN, add the following line:

SERVER_ID =
Enter fullscreen mode Exit fullscreen mode

Paste the contents of your clipboard after the equals sign. Now we can retrieve our server's ID from this variable.

Once that's done, return to the definition of on_message(). We're going to add another if statement to deal with the contents of user replies in DMs. Edit the function body so that it matches the below:

@bot.event
async def on_message(message):
    print("Saw a message...")

    if message.author == bot.user:
        return # prevent responding to self

    # NEW CODE BELOW
    # Assign roles from DM
    if isinstance(message.channel, discord.channel.DMChannel):
        await assign_roles(message)
        return
    # NEW CODE ABOVE

    # Respond to commands
    if message.content.startswith("!roles"):
        await dm_about_roles(message.author)
    elif message.content.startswith("!serverid"):
        await message.channel.send(message.channel.guild.id)
Enter fullscreen mode Exit fullscreen mode

This new if statement will check whether the message that triggered the event was in a DM channel, and if so, will run assign_roles() and then exit. Now we need to define assign_roles(). Add the following code above the definition of on_message():

async def assign_roles(message):
    print("Assigning roles...")

    languages = set(re.findall("python|javascript|rust|go|c\+\+", message.content, re.IGNORECASE))
Enter fullscreen mode Exit fullscreen mode

We can find the languages mentioned in the user replies using regular expressions: re.findall() will return a list of strings that match our expression. This way, whether the user replies with "Please add me to the Python and Go groups" or just "python go", we'll be able to assign them the right role.

We convert the list into a set in order to remove duplicates.

The next thing we need to do is deal with emoji responses. Add the following code to the bottom of the assign_roles() function:

    language_emojis = set(re.findall("\U0001F40D|\U0001F578|\U0001F980|\U0001F439|\U0001F409", message.content))
    # https://unicode.org/emoji/charts/full-emoji-list.html

    # Convert emojis to names
    for emoji in language_emojis:
        { 
            "\U0001F40D": lambda: languages.add("python"),
            "\U0001F578": lambda: languages.add("javascript"),
            "\U0001F980": lambda: languages.add("rust"),
            "\U0001F439": lambda: languages.add("go"),
            "\U0001F409": lambda: languages.add("c++")
        }[emoji]()
Enter fullscreen mode Exit fullscreen mode

In the first line, we do the same regex matching we did with the language names, but using emoji Unicode values instead of standard text. You can find a list of emojis with their codes on Unicode.org. Note that the + in this list's code should be replaced with 000 in your Python code: for example, U+1F40D becomes U0001F40D.

Once we've got our set of emoji matches in language_emojis, we loop through it and use a dictionary to add the correct name to our languages set. This dictionary has strings as values and lambda functions as keys. Finally, [emoji]() will select the lambda function for the provided key and execute it, adding a value to languages. This is similar to the switch-case syntax you may have seen in other programming languages.

We now have a full list of languages our users may wish to discuss. Add the following code below the for loop:

    if languages:
        server = bot.get_guild(SERVER_ID)

        roles = [discord.utils.get(server.roles, name=language.lower()) for language in languages]

        member = await server.fetch_member(message.author.id)
Enter fullscreen mode Exit fullscreen mode

This code first checks that the languages set contains values. If so, we use get_guild() to retrieve a Guild object corresponding to our server's ID (remember, guild means server).

We then use a list comprehension and discord.py's get() function to construct a list of all the roles corresponding to languages in our list. Note that we've used the lower() to ensure all of our strings are in lowercase.

Finally, we retrieve the member object corresponding to the user who sent us the message and our server.

We now have everything we need to assign roles. Add the following code to the bottom of the if statement, within the body of the if statement:

        try:
            await member.add_roles(*roles, reason="Roles assigned by WelcomeBot.")
        except Exception as e:
            print(e)
            await message.channel.send("Error assigning roles.")
        else:
            await message.channel.send(f"""You've been assigned the following role{"s" if len(languages) > 1 else ""} on {server.name}: { ', '.join(languages) }.""")
Enter fullscreen mode Exit fullscreen mode

The member object's add_roles() method takes an arbitrary number of role objects as positional arguments. We unpack our languages set into separate arguments using the * operator, and provide a string for the named argument reason.

Our operation is wrapped in a try-except-else block. If adding roles fails, we'll print the resulting error to our repl's console and send a generic error message to the user. If it succeeds, we'll send a message to the user informing them of their new roles, making extensive use of string interpolation.

Finally, we need to deal with the case where no languages were found in the user's message. Add an else: block onto the bottom of the if languages: block as below:

    else:
        await message.channel.send("No supported languages were found in your message.")
Enter fullscreen mode Exit fullscreen mode

Rerun your repl and return to your Discord server. Open the DM channel with your bot and try sending it one or more language names or emojis. You should receive the expected roles. You can check this by clicking on your name in the right-hand panel on your Discord server – your roles will be listed in the box that appears.

Assigned roles

Removing roles

Our code currently does not allow users to remove roles from themselves. While we could do this manually as the server owner, we've built this bot to avoid having to do that sort of thing, so let's expand our code to allow for role removal.

To keep things simple, we'll remove any roles mentioned by the user which they already have. So if a user with the "python" role writes "c++ python", we'll add the "c++" role and remove the "python" role.

Let's make some changes. Find the if languages: block in your assign_roles() function and change the code above try: to match the below:

    if languages:
        server = bot.get_guild(SERVER_ID)

        # <-- RENAMED VARIABLE + LIST CHANGED TO SET
        new_roles = set([discord.utils.get(server.roles, name=language.lower()) for language in languages]) 

        member = await server.fetch_member(message.author.id)

        # NEW CODE BELOW
        current_roles = set(member.roles)

Enter fullscreen mode Exit fullscreen mode

We replace the list of roles with a set of new roles. We also create a set of roles the user current holds. Given these two sets, we can figure out which roles to add and which to remove using set operations. Add the following code below the definition of current_roles:

        roles_to_add = new_roles.difference(current_roles)
        roles_to_remove = new_roles.intersection(current_roles)
Enter fullscreen mode Exit fullscreen mode

The roles to add will be roles that are in new_roles but not in current_roles, i.e. the difference of the sets. The roles to remove will be roles that are in both sets, i.e. their intersection.

Now we need to replace the try-except-else block with the code below:

        try:
            await member.add_roles(*roles_to_add, reason="Roles assigned by WelcomeBot.")
            await member.remove_roles(*roles_to_remove, reason="Roles revoked by WelcomeBot.")
        except Exception as e:
            print(e)
            await message.channel.send("Error assigning/removing roles.")
        else:
            if roles_to_add:
                    await message.channel.send(f"You've been assigned the following role{'s' if len(roles_to_add) > 1 else ''} on {server.name}: { ', '.join([role.name for role in roles_to_add]) }")

            if roles_to_remove:
                await message.channel.send(f"You've lost the following role{'s' if len(roles_to_remove) > 1 else ''} on {server.name}: { ', '.join([role.name for role in roles_to_remove]) }")
Enter fullscreen mode Exit fullscreen mode

This code follows the same general logic as our original block, but can remove roles as well as add them.

Finally, we need to update the bot's original DM to reflect this new functionality. Find the dm_about_roles() function and amend it as follows:

async def dm_about_roles(member):
    print(f"DMing {member.name}...")

    await member.send(
        f"""Hi {member.name}, welcome to {member.guild.name}! 

Which of these languages do you use:

* Python (🐍)
* JavaScript (πŸ•ΈοΈ)
* Rust (πŸ¦€)
* Go (🐹)
* C++ (πŸ‰)

Reply to this message with one or more of the language names or emojis above so I can assign you the right roles on our server.

Reply with the name or emoji of a language you're currently using and want to stop and I'll remove that role for you.
"""
    )
Enter fullscreen mode Exit fullscreen mode

Rerun your repl and test it out. You should be able to add and remove roles from yourself. Try inviting some of your friends to your Discord server, and have them use the bot as well. They should receive DMs as soon as they join.

Welcome message
Bot role message

Where next?

We've created a simple Discord server welcome bot. There's a lot of scope for additional functionality. Here are some ideas for expansion:

  • Include more complex logic for role assignment. For example, you could have some roles that require users to have been members of the server for a certain amount of time.
  • Have your bot automatically assign additional user roles based on behavior. For example, you could give a role to users who react to messages with the most emojis.
  • Add additional commands. For example, you might want to have a command that searches Stack Overflow, allowing members to ask programming questions from the chat.

Discord bot code can be hosted on Replit permanently, but you'll need to use an Always-on repl to keep it running 24/7.

(https://docs.replit.com/hosting/enabling-always-on) repl to keep it running 24/7.

You can find the example repl at https://replit.com/@ritza/DiscordWelcomeBot

Top comments (0)