Skip to content

Commit

Permalink
Merge branch 'V3/develop' into hyperlink-and-header
Browse files Browse the repository at this point in the history
  • Loading branch information
Kreusada authored Apr 28, 2023
2 parents 7e0f20c + 6cef840 commit d734230
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 7 deletions.
1 change: 1 addition & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@
- docs/guide_cog_creators.rst
- docs/guide_migration.rst
- docs/guide_publish_cogs.rst
- docs/guide_slash_and_interactions.rst
"Category: Docs - Install Guides":
- docs/about_venv.rst
- docs/autostart_*.rst
Expand Down
333 changes: 333 additions & 0 deletions docs/guide_slash_and_interactions.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
.. Slash Commands and Interactions
.. role:: python(code)
:language: python

===============================
Slash Commands and Interactions
===============================

This guide is going to cover on how to write a simple slash command into a Red cog.
This guide will assume that you have a working basic cog.
If you do not have a basic cog, please refer to the :ref:`getting started <getting-started>` guide.
It is also adviced to make yourself familiar with `Application Commands <https://discord.com/developers/docs/interactions/application-commands>`__ from Discord's documentation.

---------------
Getting Started
---------------

To start off, we will have to import some additional modules to our cog file.
We will be using the :class:`redbot.core.app_commands` module to create our slash commands.
Once we have imported the module, we can start creating our slash commands in our cog class.
For this example we will use a basic hello world command.

.. code-block:: python
import discord
from redbot.core import commands, app_commands
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
async def hello(self, interaction: discord.Interaction):
await interaction.response.send_message("Hello World!", ephemeral=True)
Go ahead and load your cog. Once it is loaded, we will have to enable and sync our slash commands.
We can do this by using the :ref:`[p]slash<core-command-slash>` command to manage our slash commands.
Once you have registered your slash commands, you can test them out by typing ``/hello`` in your server.

----------------------------
Slash Commands and Arguments
----------------------------

There is a lot of flexibility when it comes to slash commands.
Below we will go over some of the different stuff you can do with slash commands.

Decorators
----------
Just like with text commands, we can use decorators to modify the behaviour of our slash commands.
For example, we can use the :func:`app_commands.guild_only` decorator to make our slash command only work in guilds.

.. code-block:: python
import discord
from redbot.core import commands, app_commands
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
@app_commands.guild_only()
async def hello(self, interaction: discord.Interaction):
await interaction.response.send_message("Hello World!", ephemeral=True)
One of the more useful decorators is the :func:`app.commands.choices` decorator.
This decorator allows us to specify a list of choices for a specific argument.
This is useful for arguments that have a limited number of options.
For example, we can use this to create a command that allows us to choose between two different colors.

.. code-block:: python
from redbot.core import commands, app_commands
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
@app_commands.describe(color="The color you want to choose")
@app_commands.choices(color=[
app_commands.Choice(name="Red", value="red"),
app_commands.Choice(name="Blue", value="blue"),
])
async def color(self, interaction: discord.Interaction, color: Color):
await interaction.response.send_message(f"Your color is {color}", ephemeral=True)
The user will be shown the ``name`` of the choice, and the argument will be passed the
``value`` associated with that choice. This allows user-facing names to be prettier than
what is actually processed by the command.

Alternatively, ``Literal`` can be used if the argument does not need a different
user-facing label.

.. code-block:: python
from redbot.core import commands, app_commands
from typing import Literal
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
@app_commands.describe(color="The color you want to choose")
async def color(self, interaction: discord.Interaction, color: Literal["Red", "Blue"]):
await interaction.response.send_message(f"Your color is {color}", ephemeral=True)
Finally, an ``Enum`` subclass can be used to specify choices. When done this way, the
resulting parameter will be an instance of that enum, rather than the ``value``.

.. code-block:: python
from enum import Enum
from redbot.core import commands, app_commands
class Color(Enum):
Red = "red"
Blue = "blue"
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
@app_commands.describe(color="The color you want to choose")
async def color(self, interaction: discord.Interaction, color: Color):
await interaction.response.send_message(f"Your color is {color.value}", ephemeral=True)
Check out the full reference of decorators on Discord.py's documentation `here <https://discordpy.readthedocs.io/en/stable/interactions/api.html#decorators>`__.


Groups & Subcommands
--------------------
Slash commands can also be grouped together into groups and subcommands.
These can be used to create a more complex command structure.

.. note::
Unlike text command groups, top level slash command groups **cannot** be invoked.

.. code-block:: python
import discord
from redbot.core import commands, app_commands
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
zoo = app_commands.Group(name="zoo", description="Zoo related commands")
@zoo.command(name="add", description="Add an animal to the zoo")
@app_commands.describe(animal="The animal you want to add")
async def zoo_add(self, interaction: discord.Interaction, animal: str):
await interaction.response.send_message(f"Added {animal} to the zoo", ephemeral=True)
@zoo.command(name="remove", description="Remove an animal from the zoo")
@app_commands.describe(animal="The animal you want to remove")
async def zoo_remove(self, interaction: discord.Interaction, animal: str):
await interaction.response.send_message(f"Removed {animal} from the zoo", ephemeral=True)
Arguments
---------
As shown in some of the above examples, we can amplify our slash commands with arguments.
However with slash commands Discord allows us to do a few more things.
Such as specifically select a channel that we'd like to use in our commands,
we can do the same with roles and members.
Let's take a look at how we can do that.

.. code-block:: python
import discord
from redbot.core import commands, app_commands
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
@app_commands.describe(channel="The channel you want to mention")
async def mentionchannel(self, interaction: discord.Interaction, channel: discord.abc.GuildChannel):
await interaction.response.send_message(f"That channel is {channel.mention}", ephemeral=True)
@app_commands.command()
@app_commands.describe(role="The role you want to mention")
async def mentionrole(self, interaction: discord.Interaction, role: discord.Role):
await interaction.response.send_message(f"That role is {role.mention}", ephemeral=True)
@app_commands.command()
@app_commands.describe(member="The member you want to mention")
async def mentionmember(self, interaction: discord.Interaction, member: discord.Member):
await interaction.response.send_message(f"That member is {member.mention}", ephemeral=True)
If you try out the mentionchannel command, you will see that it currently accepts any type of channel,
however let's say we want to limit this to voice channels only.
We can do so by adjusting our type hint to :class:`discord.VoiceChannel` instead of :class:`discord.abc.GuildChannel`.

.. code-block:: python
import discord
from redbot.core import commands, app_commands
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
@app_commands.describe(channel="The channel you want to mention")
async def mentionchannel(self, interaction: discord.Interaction, channel: discord.VoiceChannel):
await interaction.response.send_message(f"That channel is {channel.mention}", ephemeral=True)
With integer and float arguments, we can also specify a minimum and maximum value.
This can also be done to strings to set a minimum and maximum length.
These limits will be reflected within Discord when the user is filling out the command.

.. code-block:: python
import discord
from redbot.core import commands, app_commands
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@app_commands.command()
@app_commands.describe(number="The number you want to say, max 10")
async def saynumber(self, interaction: discord.Interaction, number: app_commands.Range[int, None, 10]):
await interaction.response.send_message(f"Your number is {number}", ephemeral=True)
See the `Discord.py documentation <https://discordpy.readthedocs.io/en/stable/interactions/api.html#range>`__ for more information on this.


---------------
Hybrid Commands
---------------
Hybrid commands are a way to bridge the gap between text commands and slash commands.
These types of commands allow you to write a text and slash command simultaneously using the same function.
This is useful for commands that you want to be able to use in both text and slash commands.

.. note::
As with slash command groups, top level hybrid command groups **cannot** be invoked as a slash command. They can however be invoked as a text command.

.. code-block:: python
from redbot.core import commands
class MyCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
@commands.hybrid_command(name="cat")
async def cat(self, ctx: commands.Context):
await ctx.send("Meow")
@commands.hybrid_group(name="dog")
async def dog(self, ctx: commands.Context):
await ctx.send("Woof")
# As discussed above, top level hybrid command groups cannot be invoked as a slash command.
# Thus, this will not work as a slash command.
@dog.command(name="bark")
async def bark(self, ctx: commands.Context):
await ctx.send("Bark", ephemeral=True)
After syncing your cog via the :ref:`[p]slash<core-command-slash>` command, you'll be able to use the commands as both a slash and text command.

---------------------
Context Menu Commands
---------------------
Context menu commands are a way to provide a interaction via the context menu.
These are seen under ``Apps`` in the Discord client when you right click on a message or user.
Context menu commands are a great way to provide a quick way to interact with your bot.
These commands accept one arguement, the contextual ``user`` or ``message`` that was right clicked.

Setting up context commands is a bit more involved then setting up slash commands.
First lets setup our context commands in our cog.

.. code-block:: python
import discord
from redbot.core import commands, app_commands
# Important: we're building the commands outside of our cog class.
@app_commands.context_menu(name="Get message ID")
async def get_message_id(interaction: discord.Interaction, message: discord.Message):
await interaction.response.send_message(f"Message ID: {message.id}", ephemeral=True)
@app_commands.context_menu(name="Get user ID")
async def get_user_id(interaction: discord.Interaction, user: discord.User):
await interaction.response.send_message(f"User ID: {user.id}", ephemeral=True)
Once we've prepared our main cog file, we have to add a small bit of code to our ``__init__.py`` file.

.. code-block:: python
from .my_cog import get_message_id, get_user_id
async def setup(bot):
bot.tree.add_command(get_message_id)
bot.tree.add_command(get_user_id)
async def teardown(bot):
# We're removing the commands here to ensure they get unloaded properly when the cog is unloaded.
bot.tree.remove_command("Get message ID", type=discord.AppCommandType.message)
bot.tree.remove_command("Get user ID", type=discord.AppCommandType.user)
Now we're ready to sync our commands to Discord.
We can do this by using the :ref:`[p]slash<core-command-slash>` command.
Take note of the specific arguments you have to use to sync a context command.

---------------------------------
Closing Words and Further Reading
---------------------------------
If you're reading this, it means that you've made it to the end of this guide.
Congratulations! You are now prepared with the basics of slash commands for Red.
However there is a lot we didn't touch on in this guide.
Below this paragraph you'll find a list of resources that you can use to learn more about slash commands.
As always, if you have any questions, feel free to ask in the `Red support server <https://discord.gg/red>`__.

For more information on `Application Commands <https://discord.com/developers/docs/interactions/application-commands>`__ as a whole, please refer to the official Discord documentation.
Discord.py also offers documentation regarding everything discussed on this page.
You can find the documentation `here <https://discordpy.readthedocs.io/en/stable/interactions/api.html>`__.
And lastly, AbstractUmbra has a great write up of `examples <https://gist.github.com/AbstractUmbra/a9c188797ae194e592efe05fa129c57f>`__.

1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Welcome to Red - Discord Bot's documentation!

guide_migration
guide_cog_creation
guide_slash_and_interactions
guide_publish_cogs
guide_cog_creators
framework_apikeys
Expand Down
5 changes: 1 addition & 4 deletions redbot/core/commands/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,7 @@ async def send_help(
async def get_cog_help_mapping(
self, ctx: Context, obj: commands.Cog, help_settings: HelpSettings
):
if obj is None:
iterator = filter(lambda c: c.parent is None and c.cog is None, ctx.bot.commands)
else:
iterator = obj.get_commands()
iterator = filter(lambda c: c.parent is None and c.cog is obj, ctx.bot.commands)
return {
com.name: com
async for com in self.help_filter_func(ctx, iterator, help_settings=help_settings)
Expand Down
6 changes: 3 additions & 3 deletions redbot/core/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def add_command(
raise CommandAlreadyRegistered(name, None)
if key in self._context_menus:
if not override:
raise discord.errors.CommandAlreadyRegistered(name, None)
raise CommandAlreadyRegistered(name, None)
del self._context_menus[key]

self._disabled_context_menus[key] = command
Expand All @@ -97,10 +97,10 @@ def add_command(

# Handle cases where the command already is in the tree
if not override and name in self._disabled_global_commands:
raise discord.errors.CommandAlreadyRegistered(name, None)
raise CommandAlreadyRegistered(name, None)
if name in self._global_commands:
if not override:
raise discord.errors.CommandAlreadyRegistered(name, None)
raise CommandAlreadyRegistered(name, None)
del self._global_commands[name]

self._disabled_global_commands[name] = root
Expand Down

0 comments on commit d734230

Please sign in to comment.