-
Notifications
You must be signed in to change notification settings - Fork 95
Red Coding Guide V3
V2 Guide | Red Bot | Python | Red Documentation | Discord Py | Resources
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.
- Overview
- Table of Contents
- Vocabulary
- How do you make the bot say something in a text channel?
- How do you make the bot whisper / DM / PM?
- How do you make the bot send a file / show an image without a link?
- How do I capture more than one word as an argument?
- How do I put my output in a code block?
- How do I make my output have colors?
- How do I get a user's id or name?
- How do I return a user/member object from an ID?
- How do I edit a message or embed?
- How can I create an alias for my command?
- How can I send a message over 2000 characters?
- How can I send multiple messages interactively?
- How can I send a message as an embed?
- How can I make a reaction menu?
- How do I get the date a user joined my server / created their account?
- How do I limit a command to only Owner/Admins/Mods?
- How do you limit a command to be usable only in the server?
- How can I make my cog integrate with Economy?
- How do I add cooldowns to my commands?
- How can I see who has reacted to a message?
- How can I trigger an action when a user joins a server?
- How can I load/save data from my cog?
- How can I make a command cost credits to use?
- How can I bundle data with my cog?
- Resources
- 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
API Reference: Sending Messages
This replaces the v2 methods of bot.say
and bot.send_message
@commands.command()
async def test(self, ctx, *, message):
await ctx.send(message)
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)
API Reference: Sending Messages
This replaces the V2 method of bot.whisper
@commands.command()
async def test(self, ctx, *, message):
author = ctx.author
await author.send(message)
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)
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)
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)
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.
@commands.command()
async def test(self, ctx, *, message):
msg = f'```{message}```'
await ctx.send(msg)
from redbot.core.utils.chat_formatting import box
@commands.command()
async def test(self, ctx, *, message):
await ctx.send(box(message))
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
@commands.command()
async def cmd(self, ctx):
message = "```ruby\nRuby will Highlight words that are Capitalized.```"
await ctx.send(message)
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'))
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}')
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
.
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}.')
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}.')
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}.')
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")
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`.")
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)
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_interactive
will 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))
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.
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)
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 Forbidden
error 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)
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.
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)
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)
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.
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)
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.
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")
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!")
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.
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.")
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.
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.")
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.
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}.')
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.
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}.")
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.
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.
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"}
}
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)
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.")
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`.")
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}.")
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)
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`.")
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.")
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 anAttributeError
. - If a key doesn't exist when using
async with
Config will raise aKeyError
. - 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
andget_raw
for dynamic attribute access. - If you use Config, then it will work for bot owners using JSON or MongoDB
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.
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.")
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.
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.
Automate the Boring Stuff
Learn Python the Hard Way
Python Tutorial
Python Documentation
Discord API
Discordpy
Red V3
Aiohttp
Discordpy Rewrite Migration Guide
Red V3 Cog Migration Guide
While this wiki contains a lot of information, some of it may be incomplete. If the information contained here still does not answer your question, feel free to pop over to my support channel on Red - Cog Support Server.