Skip to content

Red Coding Guide V3

Redjumpman edited this page Oct 12, 2018 · 23 revisions

Coding Guide

Python Guide for Red Bot Developers

V2 Guide   |   Red Bot   |   Python   |   Red Documentation   |   Discord Py   |   Resources

Overview

This guide is not here to teach you Python. If you are serious about wanting to learn Python for writing your own cogs, I highly recommend the following resources:

This guide should serve to help give quick, simple answers to common questions and problems when writing cogs for Red Bot V3. These examples do not always provide the most efficient or fastest method for executing a particular task. Only use these examples as a baseline to build on and improve.

If there is something in this guide that you feel I could improve on or add please let me know.

Table of Contents

Vocabulary

  • Guild - Reference to a server
  • Member - A user object that belongs to the guild where a command was used.
  • User - A user object without a direct relationship with a guild.
  • Bot - The client
  • DM - Direct Message
  • PM - Private Message, just an alias for DM
  • Ctx - Short for context, it is obtained when a command is used. Contains a ton of information like where the command was issued, how it was issued, who issued it, etc.
  • Author - Typically the person who used a command.
  • Id - A unique identifier for a user, guild, channel, etc
  • Cog - Synonym for a plugin

How do you make the bot say something in a text channel?

API Reference: Sending Messages

This replaces the v2 methods of bot.say and bot.send_message

Method 1

@commands.command()
async def test(self, ctx, *, message):
    await ctx.send(message)

Method 2

Using the channel object will send the message to a specific channel. In this example, we just use the channel obtained from the context. This will result in the message being sent to the same channel it was issued in; However, any valid channel object may be used to send a message.

@commands.command()
async def test(self, ctx, *, message):
    channel = ctx.channel
    await channel.send(message)

How do you make the bot whisper / DM / PM?

API Reference: Sending Messages

This replaces the V2 method of bot.whisper

Method 1

@commands.command()
async def test(self, ctx, *, message):
    author = ctx.author
    await author.send(message)

Method 2

The example below demonstrates that, you can also use a member object from someone who did not issue the command.

import discord
@commands.command()
async def test(self, ctx, member: discord.Member, *, message):
    await member.send(message)

How do you make the bot send a file / show an image without a link?

API Reference: Sending Messages

In the below example, you may also substitute ctx with a channel or author object.

import discord
@commands.command()
async def cmd(self, ctx):
    await ctx.send(file=discord.File('file_path.png', 'filename.png'))

You may also send multiple images by creating a list of of discord.File

import discord
@commands.command()
async def cmd(self, ctx):
    my_files = [
    discord.File('file_path.png', 'filename.png'),
    discord.File('other_file_path.png, 'other_filename.png')
]
    await ctx.send('Images:', files=my_files)

How do I capture more than one word as an argument?

The * is a consume-all argument for a command. Without a leading *, the argument will fail to capture anything after a space.

NOTE: When using * in conjunction with an argument such as *, text it needs to be your last set of arguments. Otherwise the command will not be able to know when one set of arguments ends and another begins.

@commands.command()
async def commandname(self, ctx, *, text) # text can be poop. I use text cause it makes sense. You don't have to make sense.
    await ctx.send(text)

How do I put my output in a code block?

API Reference: Box Formatter

Code blocks are created by enclosing your message with tripe back ticks,```. You can manually do it by placing back ticks in your messages, or you can import the box function from Red's chat formatting module.

Method 1

@commands.command()
async def test(self, ctx, *, message):
    msg = f'```{message}```'
    await ctx.send(msg)

Method 2

from redbot.core.utils.chat_formatting import box

@commands.command()
async def test(self, ctx, *, message):
    await ctx.send(box(message))

How do I make my output have colors?

API Reference: Box Formatter

Coloring text from a code block is somewhat misleading, because it's actually syntax highlighting. Syntax highlighting is used to help programmers read code easier, so it highlights functions, classes, sometimes numbers. Different languages highlight different things. You can experiment to find which language works for you. The example below uses the programming language Ruby

Method 1

@commands.command()
async def cmd(self, ctx):
    message = "```ruby\nRuby will Highlight words that are Capitalized.```"
    await ctx.send(message)

Method 2

from redbot.core.utils.chat_formatting import box
@commands.command()
async def cmd(self, ctx):
    message = "Ruby will Highlight words that are Capitalized."
    await ctx.send(box(message, lang='ruby'))

How do I get a user's id or name?

API References: Member, Context

The ctx argument (or context) will allow you to access a bunch of meta data based on the activation (invocation) of the command. You can use ctx to very easily obtain an id or the name of the person who used the command. If you wish to get the id or name from a different user, then consider using discord.Member. If the user name or mention provided is on your server/guild, it will resolve as a member object.

import discord
@commands.command()
async def test(self, ctx, member: discord.Member):
    author_name = ctx.author.name
    author_id = ctx.author.id
    member_name = member.name
    member_id = member.id
    await ctx.send(f'Author Name: {author_name}\nAuthor ID: {author_id}\n'
                   f'Member Name: {member_name}\nMember ID: {member_id}')

How do I return a user/member object from an ID?

API References: Discord Get Utility, Discord Find Utility, Bot

Returning a user or member object from an id can be accomplished quite easily with the built-in discord utility functions. I recommend using those functions first, but I included an additional method using bot.

Method 1

If discord.utils.get does not find anything, it will return None. So you should always handle cases where this might occur. Note that, you may be providing a valid id, but that user is no longer a member of that server (guild).

import discord
@commands.command()
async def test(self, ctx, member_id: int):
    member = discord.utils.get(ctx.guild.members, id=member_id)
    if member:
        return await ctx.send(f'{member.name} was found.')
    await ctx.send(f'No member on the server match the id: {member_id}.')

Method 2

Using discord.utils.find allows you to provide a custom predicate to filter the results. It will return the first item matched, or None if nothing is found. This is really useful if you have a lot of custom conditions. However, in the example below I only demonstrate looking for an id

import discord
@commands.command()
async def test(self, ctx, member_id: int):
    member = discord.utils.find(lambda m: m.id == member_id, ctx.guild.members)
    if member:
        return await ctx.send(f'{member.name} was found.')
    await ctx.send(f'No member on the server match the id: {member_id}.')

Method 3

If for some reason you find yourself unable to use one of the above discord utility functions, you can also use an instance of the bot, either via ctx or through passing bot directly to your cog. In the example below I simply use the context version.

@commands.command()
async def cmd(self, ctx, member_id: int):
    member = ctx.bot.get_user(member_id)  # Use self.bot.get_user if you have it
    if member:
        return await ctx.send(f'{member.name} was found.')
    await ctx.send(f'No member on the server match the id: {member_id}.')

How do I edit a message or embed?

API Reference: Edit

This will attempt to edit a message or an embed. If the message was deleted before it could be edited it will raise an HTTPException. Use a try-except block in instances where the message could be deleted before an edit can be made. The following example uses an asynchronous sleep only to demonstrate that the message was edited five seconds later. This part of the code is not required.

import asyncio
@commands.command() 
async def cmd(self, ctx):
    message = await ctx.send("Hello World")
    await asyncio.sleep(5)
    await message.edit("Goodbye World")

How can I create an alias for my command?

API Reference: Command Aliases

Names and aliases are a great way to add clarity to a command, create a shortcut, or circumvent a built-in programming construct. For example list is a reserved keyword in Python. However, it may be convenient to have a command named list. The following example demonstrates this and adds some aliases as well.

NOTE: Aliases share the same namespace as other commands. Do not create an aliases name that may conflict with the name of another command.

@commands.command(name="list", aliases=['ls', 'data', 'total'])
async def _list(self, ctx):
    await ctx.send("I can be called with `list`, `ls`, `data` and `total`.")

How can I send a message over 2000 characters?

API REFERENCE: Pagify

Trying to send a message that is over 2000 characters in length will result in a HTTPException error being raised. In order to circumvent this restriction, you must split the message into chunks that do not exceed the limit. Red provides a utility called pagify to make this easy for you.

from redbot.core.utils.chat_formatting import pagify
@commands.command()
async def cmd(self, ctx):
    # The length of msg will be 2500 characters
    msg = 'I am over 2000 characters' * 100
    for page in pagify(msg):
        await ctx.send(page)

How can I send multiple messages interactively?

API Reference: Interactive Messages

Red provides a great utility in ctx that allows the user to control the flow of messages that are over 2000 characters. While the pagify utility can be great on it's own, the send_interactivewill allow the user to control the flow.

from redbot.core.utils.chat_formatting import pagify
@commands.command()
async def cmd(self, ctx):
    # The length of msg will be 2500 characters
    msg = 'I am over 2000 characters' * 100
    await ctx.send_interactive(pagify(msg))

How can I send a message as an embed?

API References: Embed, Red Embed Utility

Sending an embedded message requires you to either directly build it from scratching using discord.Embed class, or using the red utility bundled with ctx. Choosing which one you want to use, will depend on how complex you want the output to be. Use the red utility, ctx.maybe_send_embed for text without any fields, otherwise use discord.Embed for adding additional fields.

NOTE: The bot will raise a Forbidden error if it does not have the proper permissions to send and embedded message. You only need to worry about this if you build the embed with discord.Embed. Also remember that sending an embed is still subject similar conditions as normal messages and will raise an HTTPException if it is too long.

Method 1

This command will attempt to output the text as an embed, but will fallback to a simple text output if the bot does not have the required permissions.

@commands.command()
async def cmd(self, ctx):
    text = "Hello World"
    await ctx.maybe_send_embed(simple_text)

Method 2

This builds an embed with custom fields. Remember to check that the bot has sufficient permissions to send this type of output or it will raise a Forbiddenerror message. Check out the API reference for a full list of attributes.

import discord
@commands.command()
async def cmd(self, ctx):
    embed = discord.Embed(color=0xEE2222, title='New Embed')
    embed.add_field(name='title 1', value='value 1')
    embed.add_field(name='title 2', value='value 2')
    embed.set_footer(text='I am the footer!')   
    await ctx.send(embed=embed)

How can I make a reaction menu?

API References: Red Menu Utility, Pagify

This utility allows you to make a "menu" system using reactions. You can use the pre-built default controls for simple bidirectional paging and exiting, or you can map your own behavior to each reaction. You can either use a list of strings or embeds, but they most all be the same type. Consider using the Pagify utility for splitting up strings.

NOTE: The menu system is subject to the same restrictions as normal and embedded messages. Exceeding the maximum character limit will result in an HTTPException being raised, and having insufficient permissions could result in either a Permission or Forbidden error.

Example One

This example shows how you can use the menu system with a list of strings.

from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
@commands.command()
async def cmd(self, ctx):
    pages = ["page 1", "page 2", "page 3"]  # or use pagify to split a long string.
    await menu(ctx, pages, DEFAULT_CONTROLS)

Example Two

In this example a list of embeds is supplied to the menu. For brevity, the below example just creates 3 embeds using a simple for loop. Remember, that the bot will require sufficient permissions to output an embed.

import discord
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS
@commands.command()
async def cmd(self, ctx):
    embeds = []
    for x in map(str, range(1, 4)):
        embed = discord.Embed(color=0xEE2222, title=f"Page {x}")
        embeds.append(embed)
    await menu(ctx, embeds, DEFAULT_CONTROLS)

How do I get the date a user joined my server or created their account?

API References: Joined Date, Creation Date, Datetime, String Format Time

To obtain the date a member joined a server, use the joined_at method. This will return a Python Datetime object. You can also obtain the date a user or member created this discord account with the created_at method. In order to format the datetime object into something human readable, you will need to use the strftime method. Use the API reference above to view additional inputs for expressing the datetime object.

NOTE: A user object will not have a joined_at method. This is reserved for member objects because they are associated with a guild (server). However, bot member and user objects have the created_at method.

Example

The following example shows both the date a member's account was created and when they joined a server.

import discord
@commands.command()
async def cmd(self, ctx, member: discord.Member):
    joined = member.joined_at.strftime("%d %b %Y %H:%M")
    created = member.created_at.strftime("%d %b %Y %H:%M")
    msg = (f'Join date for {member.name}: {joined}\n'
           f'Account created on: {created}')
    await ctx.send(msg)

How do I limit a command to only Owner/Admins/Mods?

API Reference: Permissions Cog, Red Checks Utility

Discord py comes with some built-in functionality for this, but Red also provides similar utilities built-in. The advantage of using the Red versions, is that it will make your cog compatible with the Permissions cog. Essentially, if someone wants to alter the permissions for a particular command, they can do it via the Permissions Cog instead of having to change your code.

Example

The following example shows three separate commands, each with a different limitation. For Admin and Mod you can add a permission to bypass the role requirement. If you haven't set an admin role or a mod role for your server, you can do so with the set adminrole and set modrole respectively.

from redbot.core import checks, commands

@commands.command()
@checks.is_owner()
async def cmd1(self, ctx):
    await ctx.send("I can only be used by the bot owner.")

@commands.command()
@checks.admin_or_permissions(ban_members=True)
async def cmd2(self, ctx):
    await ctx.send("I require an admin or someone with the ban members"
                   "permission to be used")

@commands.command()
@checks.mod_or_permissions(administrator=True)
async def cmd2(self, ctx):
    await ctx.send("I require a mod or someone with the administrator"
                   "permission to be used")

How do you limit a command to be usable only in the server?

API Reference: Guild Only

Discord py has a really easy check for this that comes built-in with commands.

@commands.command()
@commands.guild_only()
async def cmd(self, ctx):
    await ctx.send("I can only be used in a guild!")

How can I make my cog integrate with Economy?

API Reference: Bank

The Economy functionality has been conveniently moved to a bank module that will allows for easy importing. Previous version of Red Bot required you to use bot.get_cog('Economy') and that has been deprecated.

Example

In this example, we simple check the balance of the author and send a message based on the amount.

from redbot.core import bank
@commands.command()
async def cmd(self, ctx):
    balance = await bank.get_balance(ctx.author)
    if balance > 1000:
        await ctx.send("You are rich!")
    else:
        await ctx.send("Looks like you are running low on funds.")

How do I add cooldowns to my commands?

API Reference: Command Cooldowns

Discord py comes with a built-in decorator to allow for easy to use cooldowns for a command. You can set it to a global, user, channel, or per guild basis. It takes three arguments:

  • Rate - The amount of times the command can be used before the cooldown is triggered.
  • Per - How long the cooldown will last once it is triggered
  • Type - Who the cool down will affect.

You may setup your own custom cool downs by recording when a command was used and checking the difference each time it is used again, but this guide does not cover such cases.

NOTE: Reloading your cog, shutting/restarting the bot, or a unexpected exit of the bot, will reset all cooldowns.

Example:

The following example shows two commands each with a different type of cooldown. The first command can only be used once by a user, before triggering a thirty second cooldown. Only the user who used the command will have this cooldown.

The second command can only be used five times before triggering a two minute cooldown. During the cooldown period no one on the server can use the command.

@commands.command()
@commands.cooldown(rate=1, per=30, type=commands.BucketType.user)
async def cmd1(self, ctx):
    await ctx.send("I can only be used once every 30 seconds, per user")

@commands.command()
@commands.cooldown(rate=5, per=120, type=commands.BucketType.guild)
async def cmd2(self, ctx):
    await ctx.send("I can only  be used five times, per guild, every 2 minutes.")

How can I see who has reacted to a message?

API References: Reactions, Message, Filter, Next, Lambda, AsyncIterator, Channel, Discord Developer Mode

In order to get the reactions for a particular message, you will need the message object. You will most likely need to convert a message id into the message object. The following example assumes you do not have the id stored somewhere, and thus allows you to input the message id . To convert a message id into a message object you must have the channel object that the message belongs to.

To see the id of a message, you will need to turn on developer mode inside of discord. Now find the message with reactions you wanted. Next, click on the three vertical dots. Then, select copy id. Now you have the message id needed for the command!

The message.reactions method will return an AsyncIterator. You will need to iterate over this using async for or flatten it to a list. In the example below, I simply flatten it into a list of users.

Example

In this example, we are going to filter out all reactions to the message, except for the 👍 emoji. This code assumes your message has been reacted to by at least one 👍. You will also need to specify the channel the message was sent in.

import discord
@commands.command()
async def cmd(self, ctx, channel: discord.TextChannel, msgid: int):
    message = await channel.get_message(msgid)
    try:
	    reaction = next(filter(lambda x: x.emoji == '\U0001F44D', msg.reactions), None)
	except AttributeError:
	    return await ctx.send("The message id provided is either invalid, "
	                          "or is not from that  channel.")
	users = await reaction.users().flatten()
    fmt = ', '.join(users)
    # This will break if it's over 2k characters!
    await ctx.send(f'The following users reacted with 👍\n{fmt}.')

How can I trigger an action when a user joins a server?

API Reference: Events Member Join

This requires an Event listener. The bot (client) will listen for specific events, and then allow you to execute some code. There are several different events you can use, but this example uses the member_join event.

Example

This assumes the event function is defined within your cog's class. It sends a DM to the user and welcome's them to the server.

async def on_member_join(self, member):
    server = member.guild.name
    await member.send(f"Welcome to {server}.")

How can I load/save data from my cog?

API Reference: Config, JSON, MongoDB

Saving data in Red Version 3 is a lot different than the Version 2 method. While this implementation may seem a bit cumbersome at first, it provides a lot of fantastic advantages over the legacy style. First, config supports both JSON and MongoDB, so you do not have to write separate code for each type of database. Also it is threadsafe and works asynchronously so you are less likely to have your bot tied up with several I/O operations. Finally, the data doesn't sit in memory the entire time.

In the legacy version (dataIO), all of your data would sit in memory the entire time the cog was loaded. When your bot loaded your cog, it would load all of your json data into memory. Changes were made in memory, and then saved to disk. When you needed to look at your data, you just needed to check your copy. This was really expensive, and caused a lot of problems on bots that were on really large servers.

One other advantage that I have found working with config, is if I decided to add new default data, all I have to do is add it to my defaults. In legacy, I would have to make the user run an consistency check every time they loaded the cog. This is a huge deal for me!

The tutorial for config in the official documentation does a good job of explaining how to set it up, but I'll try to add some additional context.

Example

In this example, we create a simple cog that let's users submit a word of the day. I broke up the cog into several parts, but you can find the entire code here.

Part 1

I start by adding random from the standard library, which I will get to later in the tutorial. But the important thing is that we add Config to our cog. Then, we have to setup our defaults using a dictionary. I create a few basic default values that will be created when the cog is loaded for the first time.

NOTE: When creating default keys for your database, you must use an underscore for instead of a space. This is because dot notation does not permit spaces. For example, a key called Hello_World can be accessed via db.Hello_World, but you can not do db.Hello World. This only applies to defaults.

import random
from redbot.core import Config
from redbot.core import commands

defaults = {"Words": [],  
            "Mode": "Unique",  
            "WOTD": None,  
            "Holidays": {"Ramadan": "Bismallah",  
                         "Halloween": "Spooky"}  
            } 

Part 2

Then, I create the init. Under __init__ You must use Config.get_conf. This will grab your configuration when the cog is loaded.

The first argument will always be self, and references the cog's main class.

The next argument, identifier is a unique number for your cog. You can input any number you want. This is used to safeguard your cog's data when it conflicts with another cog that has the same class name.

The third argument, force_registration is set to True (False is the default). What this does is make config throw an error if you try to set or retrieve a value for a key that does not exist.

Finally, we register our defaults to the guild. You can use multiple or different categories for your cogs. This simply means, that the data will unique for every guild the bot is connected to. You can find out more about the different categories in the Config documentation.

class Words: 

    def __init__(self):
        self.database = Config.get_conf(self, identifier=1234567890, force_registration=True)
        self.database.register_guild(**defaults)

Part 3

Ok. Now that we have the class and the database set up, let's add a commands to see how this works.

In this command, we create a 24 hour cooldown, per user to limit the submissions to once per day. Because the Words key in our defaults has a list as it's value, we use the context manager to manipulate it. Any time you have a mutable type you should use async with.

Also note, as you will see through-out this tutorial, the Guild object is passed with ctx.guild every time we need to get access to the guild category of the database.

    @commands.command()
    @commands.cooldown(rate=1, per=86400, type=BucketType.user)
    async def addword(self, ctx, word: str):
        async with self.database.guild(ctx.guild).Words() as words:
            words.append(word.lower())
        await ctx.send(f"{word.lower()} was added to the word list.")     

Part 4

Next, we will add a command that will let us change our mode. There will be two modes, Unique and Duplicates. Later we will make it so Unique filters out any duplicate words submitted, and Duplicates picks from the original list including dupes.

If you need to simply change the value of a default key, use the set method.

@commands.command()
    async def wordmode(self, ctx, mode: str):
        if mode.title() in ['Unique', 'Duplicates']:
            await self.database.guild(ctx.guild).Mode.set(mode.title())
            await ctx.send(f"WOTD mode changed to {mode.title()}.")
        else:
            await ctx.send(f"Mode can only be set to `Unique` or `Duplicates`.")

Part 5

Then, we will add two more commands that will allow us to set and get the holiday words. Sometimes you need to dynamically access a default value, either via user input or some other way. This is where the set_raw and get_raw methods come into play.

NOTE: You can use these at any depth, but I used them at the top level to make the example better. For example, you could write Holidays.set_raw(holiday.title(), value=word) in the first command.

@commands.command()
    async def setholiday(self, ctx, holiday: str, word: str):
        if not holiday.title() in ["Ramadan", "Halloween"]:
            return await ctx.send("Holiday must be either Ramadan or Halloween.")
        
        await self.database.guild(ctx.guild).set_raw("Holidays", holiday.title(), value=word)
        await ctx.send("{holiday}'s word was changed to {word}.")
    
    @commands.command()
    async def wordholiday(self, ctx, holiday: str):
        if not holiday.title() in ["Ramadan", "Halloween"]:
            return await ctx.send("Holiday must be either Ramadan or Halloween.")
        
        word = await self.database.guild(ctx.guild).get_raw("Holidays", holiday.title())
        await ctx.send(f"{holiday}'s word is {word}.")

Part 6

Sometimes, you may need to see a dictionary representation of either a partial part of your data, or the entire database structure. In these cases, you can use the all method. You can access the data like a normal dictionary. For example, you can do data['Holidays']['Halloween'] and it will return the value for Halloween. Please note that changing a value this way, will not change the data in your database.

    @commands.command()
    async def worddata(self, ctx):
        data = await self.database.guild(ctx.guild).all()
        await ctx.send(data) 

Part 7

If you need to reset a particular part of your database to it's default value, you can use the clear method.

NOTE: You can also use the clearall method at the top of your structure. This will reset everything in that category to it's default value.

@commands.command()  
async def clearwotd(self, ctx):  
    await self.database.guild(ctx.guild).WOTD.clear()  
    await self.database.guild(ctx.guild).Words.clear()     
    await ctx.send("WOTD cleared. A new word will be randomly "  
                   "selected the next time you run `wotd`.")

Part 8

Finally, we create a command to display the word of the day. This command will pick a random word from the list of words in our database. We also incorporate some of our other settings in the database as well, like mode.

    @commands.command()
    async def wotd(self, ctx):
        guild = ctx.guild
        if not await self.database.guild(guild).WOTD():
            mode = await self.database.guild(guild).Mode()
            async with self.database.guild(guild).Words() as words:
                if words:
                    if mode == 'Unique':
                        wotd = random.choice(set(words))
                    else:
                        wotd = random.choice(words)
                else:
                    return await ctx.send("There are no words in the list to set.")
            await ctx.send(f"The **new** Word of the Day is now {wotd}.")
        else:
            wotd = await self.database.guild(guild).WOTD()
            if wotd:
                await ctx.send("The word of the day is {wotd}.")
            else:
                await ctx.send("No word of the day has been set yet.")

Full Code

Some final notes on Config:

  • Use force_registration=True because it prevents mistakes.
  • if a key doesn't exist when using set, set_raw, get, get_raw Config will raise an AttributeError.
  • If a key doesn't exist when using async withConfig will raise a KeyError.
  • Adding new defaults later does not require a consistency check.
  • Always use async with when working with mutable types (lists and dicts).
  • Use set_raw and get_raw for dynamic attribute access.
  • If you use Config, then it will work for bot owners using JSON or MongoDB

How can I make a command cost credits to use?

API References: Custom Checks, Bank

You can create custom check decorators via commands.check. In the example below, the function charge will try to withdraw the set amount from the person who used the command. bank.withdraw will raise a ValueError if the person does not have sufficient funds, therefore we catch that with a try-except block. Finally, we return the Boolean result. The command will not run, if it returns False.

NOTE: In this example, credits will still be deducted, regardless of any errors that may occurs in the actual command.

Example

from redbot.core import bank, commands

# This will go outside the class scope
def charge(amount: int):  
    async def pred(ctx):
        try:  
            await bank.withdraw_credits(ctx.author, amount)
        except ValueError:
            return False
        else:
            return True  
    return commands.check(pred)

class CogName:

    @commands.command()
    @charge(amount=50)
    async def cmd(self, ctx):
        await ctx.send("I removed 50 credits from your account to say this.")

How can I bundle data with my cog?

API References: Data Manager, Bundled Data Path, Load Bundled Data, CSV

Sometimes you may want to have your cog bundled with some external data. Maybe some images, text files, or some other kind of data that doesn't make sense to have in your source code. But having all that work cross platform and account for users having Red saved to a non-default directory can be a mess! Thankfully, packaging and loading this external data is fairly simple.

Your cog folder should look something like this:

- cog_folder_name
  - data
    - data_file.txt
  - __init__.py
  - cog.py
  - info.json

Downloader will take care of getting everything inside your cog package. Once you have your structure complete, you will need to modify your __init__.py module. From the docs, it should look like this:

from redbot.core import data_manager

def setup(bot):
    cog = MyCog()
    data_manager.load_bundled_data(cog, __file__)
    bot.add_cog(cog)

Note: You must load the bundled data before adding the cog to the bot.

Suppose the following scenario. You have a cog called Restaurant health ratings. You scraped the ratings from a bunch of different public records websites, cleaned the data, and stuck it into a csv file. Your header row looks like this: Establishment', 'City', 'Address', 'Score', 'Grade', 'Risk Type', 'Reasons'

Let's say the row we want to pull looks like this: 'Lou's Bucket o' Grease', 'St. Louis', '1234 Heart Attack Lane', '66', 'U', '3', 'No gloves\nEmployees did not wash their hands\nEmployees working while ill\nNo display of Consumer Advisory regarding raw or uncooked foods.

We could pull this by name using Red's bundled_data_path and csv from the standard library.

Example

import csv
from redbot.core import commands
from redbot.core.data_manager import bundled_data_path

class HealthReports:
    @commands.command()
    async def inspection(self, ctx, name):
        file_path = bundled_data_path(self) / 'HealthInspections.csv'
        try:
            with file_path.open('rt') as f:
                reader = csv.DictReader(f, delimiter=',')
                for row in reader:
                    if row['Establishment'] == name:
                        return await ctx.send(**row)
        except FileNotFoundError:
            return await ctx.send('Could not find the requested file')
        await ctx.send("Could not find {name} listed in HealthInspections.") 

Let's break down some components of the above code. bundled_data_path requires that we pass an instance of the cog (which is our class). The easiest reference to that is through self. It's like bundled data is asking who you are, and you saying "I'm myself of course"! But this only navigates up to the data folder, so we add the / 'HealthInspections.csv' to show it the rest of the way.

Next we use a with statement to open the file. I use csv.DictReader so I can navigate the file like a dictionary, with the keys being the headers. It then iterates over each row until it finds a match. Finally, it returns an unpacked dictionary using **. If it does not find anything, the command will simply return that it didn't find the listed name.

Even though I used a csv file in my example, you can use other similar file types as well.

Resources

Learning Python

Automate the Boring Stuff
Learn Python the Hard Way
Python Tutorial
Python Documentation

API References

Discord API
Discordpy
Red V3
Aiohttp

Migration Guides

Discordpy Rewrite Migration Guide
Red V3 Cog Migration Guide

Clone this wiki locally