Interactions#

Pincer makes it extremely easy to create an interaction. All you need to do is create a method with the @command decorator.

Note

Although purely functional bots are possible, it’s recommended to inherit from the Client class as seen in the example below.

from pincer import Client
from pincer.commands import command

class Bot(Client):
  @command
  async def some_command(self):
    ...

bot = Bot("TOKEN")
bot.run()

This registers a command called “some_command”. It’s pretty useless right now, so let’s take a closer look at what else you can do.

Note

If you’re not seeing your command register, it’s likely because it takes up to one hour for a global slash command to do so. Specify the guild in the command decorator to have application commands register instantly.

To register your command to a guild do:

@command(guild=MY_GUILD_ID)
async def some_command(self):
    ...

Sending messages to the user is extremely easy. What you want to return is inferred by the object’s return type. A str can be returned to send a text message. Here’s a simple ping command.

@command
async def ping(self):
    return "pong"

If you need access to more information, you can pass in the ctx object.

Note

ctx and self should be those exact names or the correct value will not be passed in.

from pincer.objects import MessageContext
...

@command
async def hello(self, ctx: MessageContext):
    # Returns the name of the user that initiated the interaction
    return f"hello {ctx.author}"

Application Command Types#

Pincer provides an API for all three interaction command types. The only thing that varies is the function signature.

from pincer.commands import command, user_command, message_command
from pincer.objects import MessageContext, UserMessage, User
...

@command
# Can have any amount of inputs
async def ping(self, ctx: MessageContext, arg1: str, arg2: str):
    return "pong"

# Must have a parameter for users. User can be a GuildMember. All User
# methods are available to GuildMember because GuildMember inherits from
# User.
@user_command
async def user_ping(self, ctx: MessageContext, user: User):
    return "pong"

# Must have a parameter for messages.
@message_command
async def message_ping(self, ctx: MessageContext, message: UserMessage):
    return "pong"

Interaction Timeout#

Interactions time out after 3 seconds. To extend the timeout to 15 minutes you can run ack() from MessageContext. InteractionFlags is available in this method.

Arguments#

Every parameter besides ctx and self is inferred to be a slash command argument. Notice how word is typehinted as string. Pincer uses type hints to infer the argument type that you want.

@command
async def say(self, word: str):
    return word

The list of possible type hints is as follows:

Class

Command Argument Type

str

String

int

Integer

bool

Boolean

float

Number

User

User

Channel

Channel

Role

Role

Mentionable

Mentionable

You might want to specify more information for your arguments. If you want a description for your command, you will have to use the Description type. Modifier types like this need to be inside of the Annotated type.

from typing import Annotated # Python 3.9+
from typing_extensions import Annotated # Python 3.8

from pincer.commands import Description
from pincer.objects import MessageContext

@command
async def say(
    self,
    ctx: MessageContext,
    word: Annotated[
      str,
      Description("A word that the bot will say.")  # type: ignore # noqa: F722
    ]
):
    # Returns the name of the user that initiated the interaction
    return word

These are the available modifiers:

Modifier

What it does

Locked to types

Description

Description of a command option.

Choices

Application command choices.

str, int, float

ChannelTypes

A group of channel types that a user can pick from.

Channel

MaxValue

The maximum value for a number.

int, float

MinValue

The minimum value for a number.

int, float

Parameters will be an optional slash command arguments if they have a default value in Python.

@command
async def say(
    self,
    word: str = "apple"  # Word is optional
):
    return word

Return Types#

str isn’t the only thing you can return. For a more complex message, you can return a Message object. The message object allows you to return embeds and attachments. InteractionFlags are only available in the response if you return a Message object.

from pincer import Client, command, Embed
from pincer.objects import Message, File
...

@command
async def a_complex_message(self):
  return Message(
    "This is the message's content",
    embeds=[
      Embed(
        title="Pincer",
        description=(
          "🚀 An asynchronous python API wrapper meant to replace"
          " discord.py\n> Snappy discord api wrapper written"
          " with aiohttp & websockets"
        ).set_image(
          url="attachments://some_image.png"
        )
      )
    ],
    attachments=[
      File.from_file("path/to/a/file.png", filename="new_name.png"),
      "path/to/another/file.png" # A string is inferred to be a filepath here!
    ]
  )

Attachments can also be Pillow images if Pillow is installed.

from PIL import Image
...

attachments=[
  Image.new("RGBA", (500, 500), (255, 0, 0)), # Will automatically be named `image0.png`
  Image.new("RGB", (500, 500)), # Will automatically be named `image1.png`
  File.from_pillow_image(some_pillow_image, "this_is_the_image_name.png") # You can also do this to specify the name
]

Additionally, Pillow Images, Files, and Embeds can be returned directly without wrapping them in a Message object.

...
@command
async def a_complex_message(self):
  return Embed(
    title="Pincer",
    description=(
      "🚀 An asynchronous python API wrapper meant to replace"
      " discord.py\n> Snappy discord api wrapper written"
      " with aiohttp & websockets"
    )
  )
Possible Return Types#

Return Type

Discord Message

str

text only message

Embed

Discord embed

File

file attachment

PIL.Image.Image

single image attachment

Sending Messages Without Return#

The MessageContext object provides methods to send a response to an interaction.

from pincer.objects import MessageContext, Message

@command
async def some_command(self, ctx: MessageContext):
    await ctx.send("Hello world!") # Sends hello world as the response to the interaction
    return # No response will be sent now that the interaction has been completed

@command
async def some_other_command(self, ctx: MessageContext):
    await ctx.channel.send("Hello world!") # Sends a message in the channel
    return "Hello world 2" # This is sent because the interaction was not "used up"

Message Components#

Pincer supports buttons and select menus.

Sending a Button#

Buttons in Pincer are created with the @button decorator. This decorator creates an async function that works the same way as the @command decorator. self and ctx are optional.

Note

All message components need to be inside an ActionRow.

from pincer.commands import button, ActionRow, ButtonStyle

class Bot(Client):

  @command
  async def send_a_button(self):
    return Message(
      content="Click a button",
      components=[
        ActionRow(
          self.button_one, self.button_two
        )
      ]
    )

  @button(label="Click me!", style=ButtonStyle.PRIMARY)
  async def button_one():
    return "Button one pressed"

  @button(label="Also click me!", style=ButtonStyle.DANGER)
  async def button_two(ctx: MessageContext):
    return "Button two pressed"

When responding to a component interaction you have access to the update and defer_update_ack methods from MessageContext. update edits a message. defer_update_ack allows you to update a message more than 3 seconds after the interaction is started.

import asyncio
...

@button(label="Click me!", style=ButtonStyle.PRIMARY)
async def button_one():
  await ctx.update("You pressed button one")
  return "You pressed button one" # Nothing will be returned because the reaction was already used up

@button(label="Also click me!", style=ButtonStyle.DANGER)
async def button_two(ctx: MessageContext):
  await ctx.defer_update_ack()
  await asyncio.sleep(10)
  await ctx.update("Button two was pressed 10 seconds ago")

Link buttons can be sent with the LinkButton class.

from pincer.commands import LinkButton

@command
async def send_a_button(self):
  return Message(
    content="Click a button",
    components=[
      ActionRow(
        LinkButton(label="Check out Pincer!", url="https://pincer.dev/")
      )
    ]
  )

Select Menus#

Select menus work similarly to Buttons. Select menus support all available methods on ctx.

from pincer.commands import button, ActionRow, ButtonStyle

class Bot(Client):

  @command
  async def send_a_select_menu(self):
    return Message(
      content="Choose an option",
      components=[
        ActionRow(
          self.select_menu
        )
      ]
    )

  @select_menu(options=[
      SelectOption(label="Option 1"),
      SelectOption(label="Option 2", value="value different than label")
  ])
  async def select_menu(values: List[str]):
    return f"{values[0]} selected"

You can also dynamically set the selectable options.

@command
async def send_a_button(self):
  return Message(
    content="Choose an option",
    components=[
      ActionRow(
        self.select_menu.with_options(
            SelectOption(label="Option 1"),
            SelectOption(label="Option 2")
        )
      )
    ]
  )

@select_menu
async def select_menu(values: List[str]):
  return f"{values[0]} selected"

Subcommands and Subcommand Groups#

To nest commands, Pincer organizes them into Group and Subgroup objects. Group and Subgroup names must only consist of lowercase letters and underscores.

This chart shows the organization of nested commands:

If you use a group:

group name
  command name

If you use a group and sub group:

group name
  subgroup name
    command name

Organizing commands like this is also valid:

group name
  subgroup name
    command name
  command name

Group and Subgroup are set to the parent in a @command decorator to nest a command inside of them. They are not available for User Commands and Message Commands.

from pincer.commands import Group, Subgroup
...

class Bot(Client):

  command_group = Group("command_group")
  command_sub_group = Subgroup("command_sub_group", parent=a_command_group)

  @command(parent=command_group)
  def command_group_command():
    pass

  @command(parent=command_sub_group)
  def command_sub_group_command():
    pass

  # Creating these commands is valid because there is no top-level command or group
  # with the same name.
  @command
  def command_group_command():
    pass
  @command
  def command_sub_group():
    pass

  # This command is not valid because there is a group with the same name.
  @command
  def a_command_group():
    pass