-
Notifications
You must be signed in to change notification settings - Fork 94
Discord Coding Guide
NOTE: THIS GUIDE IS FOR RED V2 AND DISCORD.PY V0.16.6
If you are looking for my Red V3 Coding Guide go there.
The purpose of this guide is not to teach you Python, but to help you better understand how Red interacts with the Discord Python wrapper, and to answer common questions. If you have something you think I should add to this guide, let me know. Remember, there is always more than one way to skin a cat. Not all of the examples below are the best, or the only way to achieve the end result, but they will work.
If you find these examples helpful star my repo above and let me know what I might be able to add to help in the future.
- Say
- Whisper
- Upload Image
- Make commands usable in DMs
- Integration
- Permissions
- Group Commands
- Multi Word Arguments
- Handling 3rd Party Requirements
- Command Aliases
- Code Blocks
- Colored Text
- Saving Data
- Get User Info
- Non-Blocking Code
- Formatted Strings
- Convert User ID to User Object
- Targeted Messaging
- Embeds
- Logging
- Custom Exceptions
- Editing Messages
@commands.command()
async def test(self, *, message):
await self.bot.say(message)
Method 1 (Whispering the person who used the command)
@commands.command()
async def test(self, *, message):
await self.bot.whisper(message)
Method 2 (Whispering a specific user)
import discord # We need this because we will use it in the command
@commands.command() # Here we are getting the member object
async def commandname(self, ctx, user: discord.Member):
await self.bot.send_message(user, message)
@commands.command()
async def commandname(self, ctx):
channel = ctx.message.channel
with open('data/cogfolder/imagename.png', 'rb') as f:
await self.bot.send_file(channel, f)
@commands.command(no_pm=True) # Default is True, and you can just omit it from the arguments as well.
When using another cog for "integration" you are essentially calling the functions that were defined in that cog and executing them. This ensures you do not alter or corrupt anything if the cog changes. I use economy as an example because it is very integration friendly; However, any cog can be used for integration. For a full list of economy bank functions check the code here
Example
@commands.command(pass_context=True)
async def commandname(self, ctx):
user = ctx.message.author # Member object of command issuer. Required argument in the economy function.
bank = self.bot.get_cog("Economy").bank # we use .bank at the end to specify that we want to use the bank class
bank.withdraw_credits(user, credits) # This function requires two arguments: Member object, and an integer/float
Redbot is packaged with some nice helper functions that allow us to perform a variety of checks to authorize the execution of a command.
from .utils import checks
@commands.command()
# This under a command will check if the user is an admin or it has the manage server permission
@checks.admin_or_permissions(manage_server=True)
Other examples:
@checks.mod_or_permissions()
@checks.is_owner()
serverowner_or_permissions()`
In server settings on your discord server, look in Roles.
You will see on the right: "GENERAL PERMISSIONS"
These can be used to pass as keyword arguments.
For more information look at checks.py in your utils folder located in cogs/utils
Method 1
@commands.group(name="example", pass_context=True)
async def _example(self, ctx):
"""Tell users what your group command is all about here"""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
@_example.command()
async def poop(self, ctx):
# Put what your command does here
# This command executes as !example poop
Method 2
@commands.group(pass_context=True)
async def example2(self, ctx):
"""Example2 group command is all about stuff"""
if ctx.invoked_subcommand is None:
await send_cmd_help(ctx)
@example2.command(name="poop", pass_context=False)
async def _poop_example2(self):
# Put what your command does here
# This command executes as !example2 poop
async def commandname(self, *, text) # text can be poop. I use text cause it makes sense. You don't have to make sense.
Note:
Passing the asterisk,
*
, will "consume" and assign everything to the next argument. All other positional arguments should be put before the asterisk.
# DON'T DO THIS.
# sentence consumes everything, thus word will never be used.
async def commandname(self, *, sentence, word)
# DO THIS
# Positional argument word is before *, thus it will be used.
async def commandname(self, word, *, sentence)
@command.commands(name="poop", aliases=["poo", "number2", "duty"]) # these aliases cant be used as other command names
The best way to ensure that your third-party requirements are installed by the user is to put the information in your info.json file. The libraries needed will be listed in the requirements parameter. If you want to add an additional check in your code, you can do the following:
Example using BeautifulSoup4
try: # check if BeautifulSoup4 is installed
from bs4 import BeautifulSoup
soupAvailable = True # This is like using a flag or a switch that we use later
except ImportError:
soupAvailable = False
# at the very bottom in your setup...
def setup(bot):
if soupAvailable: # This is our "switch" when it is on (meaning they have bs4 installed) it will try to load.
n = Pokedex(bot)
bot.add_cog(n)
else:
raise RuntimeError("You need to run 'pip3 install beautifulsoup4'") # This should be the instructions they need.
message = "```Hello World```"
await self.bot.say(message)
# Use parenthesis for large blocks
message = ("```"
"Hello World"
"```")
await self.bot.say(message)
# Use augmented assignment with conditionals
message = "Hello World"
if message == "Hello World":
message += "\nFirst if statement was a match!"
else:
message += "\nElse clause was met!"
await self.bot.say(message)
This is kind of misleading, because the colors are from 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 works for you. The example below uses the programming language Ruby
message = "```Ruby" + "\n" # \n is a line break, and forces the following text to go to the next line
message += "Ruby will Highlight words that are Capitalized"
message += "```"
await self.bot.say(message)
Sample Output
Ruby will Highlight words that are Capitalized
JSON is a file type which allows you to store key, value pairs and can be deeply nested. The data must be serialiazable in order for the data to be saved to JSON. Please use this table for reference when loading and unloading data:
JSON | Python |
---|---|
object | dict |
array | list |
string | str |
number (int) | int |
number (real) | float |
true | True |
false | False |
null | None |
Using DataIO you can read and write to these files. This is extremely useful when you have data that needs to persist through crashes or restarts. You should always inform the user that your cog will create one of these files. Here is an example of a JSON file created from my cog Russian Roulette:
{
"Config" : {
"Min Bet" : 50
},
"Players" : {},
"System" : {
"Active" : false,
"Player Count" : 0,
"Pot" : 0,
"Roulette Initial" : false,
"Start Bet" : 0
}
}
There are a lot of steps to this process, but here are the minimum you would need:
import os # This is required because you will be creating folders/files
from .utils.dataIO import dataIO # This is pulled from Red's utils
def __init__(self, bot):
self.bot = bot
self.file_path = "data/cogfolder/filename.json"
self.json_data = dataIO.load_json(self.file_path)
# all your commands and code
# would be the meat in this sandwich
# Right here. MMmmm. Tasty
# Substitute cogfolder with the name of the folder you want created
def check_folders(): # This is how you make your folder that will hold your data for your cog
if not os.path.exists("data/cogfolder"): # Checks if it exists first, if it does, then nothing executes
print("Creating data/cogfolder folder...") # You can put what you want here. Prints in console for the owner
os.makedirs("data/cogfolder") # This makes the directory
def check_files(): # This is how you check if your file exists and let's you create it
system = {"System": {"Pot": 0, # system is only used when you want your JSON to have a default structure
"Active": False,
"Start Bet": 0,
"Roulette Initial": False,
"Player Count": 0},
"Players": {},
"Config": {"Min Bet": 50}}
f = "data/cogfolder/filename.json" # f is the path to the file
if not dataIO.is_valid_json(f): # Checks if file in the specified path exists
print("Creating default filename.json...") # Prints in console to let the user know we are making this file
dataIO.save_json(f, system) # If you didn't use a default structure you could type: dataIO.save_json(f, {})
def setup(bot):
check_folders() # runs the folder check on setup that way it exists before running the cog
check_files() # runs the check files function to make sure the files you have exists
n = Russianroulette(bot)
bot.add_cog(n)
see more on this here
In this example we set a variable "author" as the author object. We obtain this through passing ctx and accessing that information. ctx must always be the first argument when passed (after self). We set user to be the user object for the discord member.
@commands.command(pass_context=True)
async def mycommand(self, ctx, user: discord.Member):
author = ctx.message.author
author.id # This is your author id number
user.id # This is the user's id
author.name # This is the author's id
user.name # This is the user's name
color = "Red"
animal = "Tiger"
output = "I have a {} shirt, with a huge {} on the back".format(color, animal)
await self.bot.say(output)
Something that is 'blocking' means that the bot will stop functioning until it completes that task. Two good examples are requests and time.sleep(). If you use time.sleep(5) to wait for 5 seconds, the bot will be unable to perform other tasks while it is waiting. Instead use asyncio like this:
import asyncio
await asyncio.sleep(5) # forces only the code for the command to yield until it is finished executing
In place of requests use aiohttp
@commands.command(pass_context=True)
async def mycommand(self, ctx):
server = ctx.message.server
user = ctx.message.author
member_object = server.get_member(user.id)
import discord
from discord.ext import commands
@commands.command(pass_context=True)
async def mycommand(self, ctx, role: discord.Member):
destinations = [m for m in ctx.message.server.members if role in m.roles] # list comprehension to filter members
for destination in destinations: # Loop through our list of objects, to send a message one by one
await self.bot.send_message(destination, message)
Example:
@commands.command(pass_context=True)
async def excom(self, ctx):
"""CTX example command"""
author = ctx.message.author
description = ("Short little description with a link to "
"the [guide](https://github.com/Redjumpman/Jumper-Cogs/wiki/Discord-Coding-Guide)")
field_name = "Generic Name"
field_contents = "Example contents for this field"
footer_text = "Hi. I am a footer text. I look small when displayed."
embed = discord.Embed(colour=0xFF0000, description=description) # Can use discord.Colour()
embed.title = "Cool title for my embed"
embed.set_author(name=str(author.name), icon_url=author.avatar_url)
embed.add_field(name=field_name, value=field_contents) # Can add multiple fields.
embed.set_footer(text=footer_text)
await self.bot.say(embed=embed)
There are many ways in which to implement logging. For the full details and options at your disposal please check out the docs here.
Your setup should look something like this:
def setup(bot):
global logger # Set logger as a global var
logger = logging.getLogger("red.cog_name")
if logger.level == 0: # Prevents the logger from being loaded again
logger.setLevel(logging.INFO)
handler = logging.handlers.FileHandler(filename='data/cog_name/cog_name.log', encoding='utf-8', mode='a')
handler.setFormatter(logging.Formatter('%(asctime)s %(name)-12s %(message)s',
datefmt="[%d/%m/%Y %H:%M]"))
logger.addHandler(handler)
# Make sure this is outside the if statement!
bot.add_cog(CogName(bot))
Example of adding logging to a command:
@commands.command(pass_context=True)
async def mycommand(self, ctx):
author = ctx.message.author
await self.bot.say("Hello World")
# Will the users name and the message in a text file for later review
logger.info("{} used the hello world command.".format(author.name))
Custom exceptions are great for cogs that have integration or are very complex and need to provide additional context to the user for why something did not return what they expected. Here is one way you can implement this feature in your cogs. For more information please refer to User-defined Exceptions section in the Python docs
Here is an extremely basic example:
# This will be passed into all of your specific exceptions
class MyCogError(Exception):
pass
class InvalidPassword(MyCogError):
pass
class MyCog:
def __init__(self, bot):
self.bot = bot
@commands.command()
async def mycommand(self, password):
try:
self.pass_checker(password)
except InvalidPassword: # When our custom exception is raised, we can catch it here
return await self.bot.say("Password was invalid, must be integers.")
# Rest of your password code here
def pass_checker(self, password):
# some cool pasword checking code here
try:
int(password)
except ValueError:
# Instead of raising a value error, we can raise our custom error
raise InvalidPassword()
Here is one example to do it all within the same command
@commands.command()
async def mycommand(self, password):
# Assign the message to a variable
hello = await self.bot.say("Hello World")
# Optional wait 5 seconds
asyncio.sleep(5)
# The first argument must be the message object you wish to edit
edit_message(hello, new_content="Goodbye World")
For more information on embeded messages, please check out this page on the discord py docs.
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.