A modern, feature-rich, and async-ready API wrapper for Discord written in Python
—
A comprehensive prefix-based command system built on top of the core Discord.py client. The commands framework provides command parsing, argument conversion, permission checks, cooldowns, error handling, and modular organization through cogs.
Command bots extend the base client with command processing capabilities and customizable prefix handling.
class Bot(commands.Bot):
"""
Bot client with command processing capabilities.
Parameters:
- command_prefix: String or callable returning command prefix(es)
- help_command: Help command instance (None to disable)
- description: Bot description for help
- intents: Gateway intents
- case_insensitive: Whether commands are case insensitive
- strip_after_prefix: Whether to strip whitespace after prefix
"""
def __init__(
self,
command_prefix: Union[str, List[str], Callable],
*,
help_command: Optional[HelpCommand] = ...,
description: str = None,
intents: Intents,
case_insensitive: bool = False,
strip_after_prefix: bool = False,
**options
): ...
# Command registration
def command(self, name: str = None, **attrs) -> Callable:
"""Decorator to register a command."""
def group(self, name: str = None, **attrs) -> Callable:
"""Decorator to register a command group."""
def check(self, predicate: Callable) -> Callable:
"""Decorator to add a global check."""
def before_invoke(self, coro: Callable) -> Callable:
"""Decorator for global before-invoke hook."""
def after_invoke(self, coro: Callable) -> Callable:
"""Decorator for global after-invoke hook."""
# Command management
def add_command(self, command: Command) -> None:
"""Add a command to the bot."""
def remove_command(self, name: str) -> Optional[Command]:
"""Remove a command from the bot."""
def get_command(self, name: str) -> Optional[Command]:
"""Get a command by name."""
# Cog management
async def add_cog(self, cog: Cog) -> None:
"""Add a cog to the bot."""
async def remove_cog(self, name: str) -> Optional[Cog]:
"""Remove a cog from the bot."""
def get_cog(self, name: str) -> Optional[Cog]:
"""Get a cog by name."""
# Command processing
async def get_prefix(self, message: Message) -> List[str]:
"""Get valid prefixes for a message."""
async def get_context(self, message: Message, *, cls: Type = Context) -> Context:
"""Get command context for a message."""
async def invoke(self, ctx: Context) -> None:
"""Invoke a command from context."""
async def process_commands(self, message: Message) -> None:
"""Process message for commands."""
# Properties
commands: Set[Command] # All registered commands
cogs: Dict[str, Cog] # All loaded cogs
extensions: Dict[str, types.ModuleType] # Loaded extensions
class AutoShardedBot(Bot):
"""
Bot with automatic sharding capabilities.
"""
def __init__(self, command_prefix, **kwargs): ...
shard_count: Optional[int] # Number of shards
shards: Dict[int, ShardInfo] # Shard information
# Prefix helpers
def when_mentioned(bot: Bot, message: Message) -> List[str]:
"""Return bot mention as prefix."""
def when_mentioned_or(*prefixes: str) -> Callable:
"""Return bot mention or specified prefixes."""Context objects provide information about command invocation including the message, channel, guild, and author.
class Context:
"""
Command invocation context containing message and environment info.
"""
message: Message # Message that triggered the command
bot: Bot # Bot instance
args: List[Any] # Parsed command arguments
kwargs: Dict[str, Any] # Parsed keyword arguments
prefix: str # Prefix used to invoke command
command: Optional[Command] # Command being invoked
invoked_with: Optional[str] # String used to invoke command
invoked_parents: List[str] # Parent command names for subcommands
invoked_subcommand: Optional[Command] # Subcommand being invoked
subcommand_passed: Optional[str] # String passed for subcommand
command_failed: bool # Whether command failed
# Shortcuts to message properties
author: Union[User, Member] # Command author
guild: Optional[Guild] # Guild if invoked in guild
channel: Union[TextChannel, DMChannel, GroupChannel] # Channel
me: Union[ClientUser, Member] # Bot user/member
voice_client: Optional[VoiceClient] # Voice client if connected
# Utility methods
async def send(
self,
content: str = None,
*,
embed: Embed = None,
embeds: List[Embed] = None,
file: File = None,
files: List[File] = None,
view: View = None,
ephemeral: bool = False,
reference: MessageReference = None,
**kwargs
) -> Message:
"""Send a message to the context channel."""
async def reply(self, content: str = None, **kwargs) -> Message:
"""Reply to the context message."""
def typing(self) -> Typing:
"""Return typing context manager."""
async def invoke(self, command: Command, *args, **kwargs) -> Any:
"""Invoke another command."""
async def reinvoke(self, *, call_hooks: bool = False, restart: bool = True) -> None:
"""Re-invoke the current command."""
def valid(self) -> bool:
"""Check if context is valid for command invocation."""
@property
def clean_prefix(self) -> str:
"""Prefix with bot mention resolved to nickname."""
@property
def cog(self) -> Optional[Cog]:
"""Cog that contains the command."""Command objects represent individual bot commands with argument parsing, checks, and execution logic.
class Command:
"""
Represents a bot command.
"""
def __init__(self, func: Callable, **kwargs): ...
name: str # Command name
callback: Callable # Command function
help: Optional[str] # Help text
brief: Optional[str] # Brief description
usage: Optional[str] # Usage string
aliases: List[str] # Command aliases
enabled: bool # Whether command is enabled
hidden: bool # Whether command is hidden from help
checks: List[Callable] # Command checks
description: str # Long description
signature: str # Command signature with parameters
qualified_name: str # Full command name including parents
parents: List[Group] # Parent command groups
cog: Optional[Cog] # Cog containing this command
params: Dict[str, inspect.Parameter] # Command parameters
async def __call__(self, context: Context, *args, **kwargs) -> Any:
"""Invoke the command."""
async def invoke(self, context: Context, *args, **kwargs) -> Any:
"""Invoke the command with context."""
async def reinvoke(self, context: Context, *, call_hooks: bool = False) -> None:
"""Re-invoke the command."""
def error(self, coro: Callable) -> Callable:
"""Decorator for command error handler."""
def before_invoke(self, coro: Callable) -> Callable:
"""Decorator for before-invoke hook."""
def after_invoke(self, coro: Callable) -> Callable:
"""Decorator for after-invoke hook."""
def add_check(self, func: Callable) -> None:
"""Add a check to the command."""
def remove_check(self, func: Callable) -> bool:
"""Remove a check from the command."""
def copy(self) -> Command:
"""Create a copy of the command."""
class Group(Command):
"""
Command that can contain subcommands.
"""
def __init__(self, **attrs): ...
commands: Set[Command] # Subcommands
invoke_without_subcommand: bool # Whether to invoke if no subcommand
case_insensitive: bool # Case insensitive subcommand matching
def command(self, *args, **kwargs) -> Callable:
"""Decorator to add a subcommand."""
def group(self, *args, **kwargs) -> Callable:
"""Decorator to add a subcommand group."""
def add_command(self, command: Command) -> None:
"""Add a subcommand."""
def remove_command(self, name: str) -> Optional[Command]:
"""Remove a subcommand."""
def get_command(self, name: str) -> Optional[Command]:
"""Get a subcommand by name."""
def walk_commands(self) -> Iterator[Command]:
"""Recursively walk all commands."""
# Command decorators
def command(name: str = None, **attrs) -> Callable:
"""Decorator to create a command."""
def group(name: str = None, **attrs) -> Callable:
"""Decorator to create a command group."""Cogs provide modular organization for commands, event listeners, and shared functionality.
class Cog:
"""
Base class for command cogs (modules).
"""
def __init_subclass__(cls, **kwargs): ...
qualified_name: str # Cog name
description: Optional[str] # Cog description
def get_commands(self) -> List[Command]:
"""Get all commands in this cog."""
def walk_commands(self) -> Iterator[Command]:
"""Recursively walk all commands in this cog."""
def get_listeners(self) -> List[Tuple[str, Callable]]:
"""Get all event listeners in this cog."""
# Lifecycle hooks
async def cog_load(self) -> None:
"""Called when cog is loaded."""
async def cog_unload(self) -> None:
"""Called when cog is unloaded."""
def cog_check(self, ctx: Context) -> bool:
"""Global check for all commands in this cog."""
async def cog_command_error(self, ctx: Context, error: Exception) -> None:
"""Error handler for all commands in this cog."""
async def cog_before_invoke(self, ctx: Context) -> None:
"""Before-invoke hook for all commands in this cog."""
async def cog_after_invoke(self, ctx: Context) -> None:
"""After-invoke hook for all commands in this cog."""
class GroupCog(Cog):
"""
Cog that acts as a command group.
"""
group_name: str # Name of the group
group_description: str # Description of the group
# Inherits Group functionality for subcommandsConverters transform string arguments into Python objects with built-in support for Discord entities and custom conversion logic.
class Converter:
"""
Base class for argument converters.
"""
async def convert(self, ctx: Context, argument: str) -> Any:
"""Convert string argument to desired type."""
raise NotImplementedError
# Built-in converters
class MemberConverter(Converter):
"""Convert to Member object via ID, mention, or name."""
async def convert(self, ctx: Context, argument: str) -> Member: ...
class UserConverter(Converter):
"""Convert to User object via ID, mention, or name."""
async def convert(self, ctx: Context, argument: str) -> User: ...
class TextChannelConverter(Converter):
"""Convert to TextChannel via ID, mention, or name."""
async def convert(self, ctx: Context, argument: str) -> TextChannel: ...
class VoiceChannelConverter(Converter):
"""Convert to VoiceChannel via ID, mention, or name."""
async def convert(self, ctx: Context, argument: str) -> VoiceChannel: ...
class CategoryChannelConverter(Converter):
"""Convert to CategoryChannel via ID or name."""
async def convert(self, ctx: Context, argument: str) -> CategoryChannel: ...
class RoleConverter(Converter):
"""Convert to Role via ID, mention, or name."""
async def convert(self, ctx: Context, argument: str) -> Role: ...
class GuildConverter(Converter):
"""Convert to Guild via ID or name."""
async def convert(self, ctx: Context, argument: str) -> Guild: ...
class EmojiConverter(Converter):
"""Convert to Emoji via ID, mention, or name."""
async def convert(self, ctx: Context, argument: str) -> Emoji: ...
class PartialEmojiConverter(Converter):
"""Convert to PartialEmoji via ID, mention, name, or unicode."""
async def convert(self, ctx: Context, argument: str) -> PartialEmoji: ...
class MessageConverter(Converter):
"""Convert to Message via ID or message link."""
async def convert(self, ctx: Context, argument: str) -> Message: ...
class InviteConverter(Converter):
"""Convert to Invite via invite code or URL."""
async def convert(self, ctx: Context, argument: str) -> Invite: ...
class ColourConverter(Converter):
"""Convert to Colour via hex, RGB, or name."""
async def convert(self, ctx: Context, argument: str) -> Colour: ...
ColorConverter = ColourConverter # Alias
class clean_content(Converter):
"""
Convert message content with mentions resolved.
Parameters:
- fix_channel_mentions: Whether to fix channel mentions
- use_nicknames: Whether to use nicknames for users
- escape_markdown: Whether to escape markdown
- remove_markdown: Whether to remove markdown
"""
def __init__(
self,
*,
fix_channel_mentions: bool = False,
use_nicknames: bool = True,
escape_markdown: bool = False,
remove_markdown: bool = False
): ...
class Greedy:
"""
Converter that consumes multiple arguments.
Usage: async def command(ctx, users: commands.Greedy[Member]):
"""
def __init__(self, converter: Type): ...
class Range:
"""
Converter that validates numeric ranges.
Parameters:
- min: Minimum value (inclusive)
- max: Maximum value (inclusive)
Usage: async def command(ctx, number: commands.Range[int, 1, 10]):
"""
def __init__(self, annotation: Type, min: Any = None, max: Any = None): ...
# Converter utilities
async def run_converters(ctx: Context, converter: Type, argument: str, param: inspect.Parameter) -> Any:
"""Run converter on argument."""Checks provide permission and condition validation before command execution.
# Check decorators
def check(predicate: Callable[[Context], bool]) -> Callable:
"""Add a check to a command."""
def check_any(*checks: Callable) -> Callable:
"""Require any of the provided checks to pass."""
# Permission checks
def has_role(item: Union[int, str]) -> Callable:
"""Check if user has a specific role."""
def has_any_role(*items: Union[int, str]) -> Callable:
"""Check if user has any of the specified roles."""
def has_permissions(**perms: bool) -> Callable:
"""Check if user has specific permissions."""
def has_guild_permissions(**perms: bool) -> Callable:
"""Check if user has specific guild permissions."""
def bot_has_role(item: Union[int, str]) -> Callable:
"""Check if bot has a specific role."""
def bot_has_any_role(*items: Union[int, str]) -> Callable:
"""Check if bot has any of the specified roles."""
def bot_has_permissions(**perms: bool) -> Callable:
"""Check if bot has specific permissions."""
def bot_has_guild_permissions(**perms: bool) -> Callable:
"""Check if bot has specific guild permissions."""
# Context checks
def dm_only() -> Callable:
"""Command can only be used in DMs."""
def guild_only() -> Callable:
"""Command can only be used in guilds."""
def is_owner() -> Callable:
"""Check if user is bot owner."""
def is_nsfw() -> Callable:
"""Check if channel is NSFW."""
# Hook decorators
def before_invoke(coro: Callable) -> Callable:
"""Register before-invoke hook."""
def after_invoke(coro: Callable) -> Callable:
"""Register after-invoke hook."""Cooldown system prevents command spam and implements various rate limiting strategies.
class BucketType(Enum):
"""Cooldown bucket types."""
default = 0 # Global cooldown
user = 1 # Per-user cooldown
guild = 2 # Per-guild cooldown
channel = 3 # Per-channel cooldown
member = 4 # Per-member (user+guild) cooldown
category = 5 # Per-category cooldown
role = 6 # Per-role cooldown
class Cooldown:
"""
Represents a command cooldown.
Parameters:
- rate: Number of times command can be used
- per: Time period in seconds
"""
def __init__(self, rate: int, per: float): ...
rate: int # Number of uses allowed
per: float # Time period in seconds
def get_tokens(self, current: float = None) -> int:
"""Get available tokens."""
def update_rate_limit(self, current: float = None) -> Optional[float]:
"""Update rate limit and return retry after time."""
def reset(self) -> None:
"""Reset the cooldown."""
def copy(self) -> Cooldown:
"""Create a copy of the cooldown."""
class CooldownMapping:
"""Maps cooldown buckets to cooldown instances."""
def __init__(self, original: Cooldown, type: BucketType): ...
def copy(self) -> CooldownMapping:
"""Create a copy of the mapping."""
def get_bucket(self, message: Message, current: float = None) -> Cooldown:
"""Get cooldown bucket for message."""
def update_rate_limit(self, message: Message, current: float = None) -> Optional[float]:
"""Update rate limit for message."""
class DynamicCooldownMapping(CooldownMapping):
"""Cooldown mapping with dynamic cooldown function."""
def __init__(self, cooldown_func: Callable[[Message], Cooldown], type: BucketType): ...
class MaxConcurrency:
"""
Limit concurrent command executions.
Parameters:
- number: Maximum concurrent executions
- per: Bucket type for concurrency limit
- wait: Whether to wait for slot availability
"""
def __init__(self, number: int, *, per: BucketType, wait: bool = False): ...
# Cooldown decorators
def cooldown(rate: int, per: float, type: BucketType = BucketType.default) -> Callable:
"""Add a cooldown to a command."""
def dynamic_cooldown(cooldown: Callable[[Message], Cooldown], type: BucketType = BucketType.default) -> Callable:
"""Add a dynamic cooldown to a command."""
def max_concurrency(number: int, *, per: BucketType = BucketType.default, wait: bool = False) -> Callable:
"""Limit concurrent command executions."""Comprehensive error handling for commands with specific exception types for different failure scenarios.
# Base command errors
class CommandError(DiscordException):
"""Base exception for command errors."""
pass
class UserInputError(CommandError):
"""Base exception for user input errors."""
pass
class CommandNotFound(CommandError):
"""Command was not found."""
pass
class DisabledCommand(CommandError):
"""Command is disabled."""
pass
class CommandInvokeError(CommandError):
"""Exception occurred during command execution."""
def __init__(self, e: Exception): ...
original: Exception
# Argument errors
class MissingRequiredArgument(UserInputError):
"""Required argument is missing."""
def __init__(self, param: inspect.Parameter): ...
param: inspect.Parameter
class TooManyArguments(UserInputError):
"""Too many arguments provided."""
pass
class BadArgument(UserInputError):
"""Argument could not be converted."""
pass
class BadUnionArgument(UserInputError):
"""Union argument could not be converted."""
def __init__(self, param: inspect.Parameter, converters: List[Type], errors: List[CommandError]): ...
param: inspect.Parameter
converters: List[Type]
errors: List[CommandError]
class BadLiteralArgument(UserInputError):
"""Literal argument is not one of the valid choices."""
def __init__(self, param: inspect.Parameter, literals: List[Any], errors: List[CommandError]): ...
# Check failures
class CheckFailure(CommandError):
"""Command check failed."""
pass
class CheckAnyFailure(CheckFailure):
"""All checks in check_any failed."""
def __init__(self, checks: List[Callable], errors: List[CommandError]): ...
checks: List[Callable]
errors: List[CommandError]
class PrivateMessageOnly(CheckFailure):
"""Command can only be used in private messages."""
pass
class NoPrivateMessage(CheckFailure):
"""Command cannot be used in private messages."""
pass
class NotOwner(CheckFailure):
"""User is not the bot owner."""
pass
class MissingRole(CheckFailure):
"""User is missing required role."""
def __init__(self, missing_role: Union[str, int]): ...
missing_role: Union[str, int]
class BotMissingRole(CheckFailure):
"""Bot is missing required role."""
def __init__(self, missing_role: Union[str, int]): ...
missing_role: Union[str, int]
class MissingAnyRole(CheckFailure):
"""User is missing any of the required roles."""
def __init__(self, missing_roles: List[Union[str, int]]): ...
missing_roles: List[Union[str, int]]
class BotMissingAnyRole(CheckFailure):
"""Bot is missing any of the required roles."""
def __init__(self, missing_roles: List[Union[str, int]]): ...
missing_roles: List[Union[str, int]]
class MissingPermissions(CheckFailure):
"""User is missing required permissions."""
def __init__(self, missing_permissions: List[str]): ...
missing_permissions: List[str]
class BotMissingPermissions(CheckFailure):
"""Bot is missing required permissions."""
def __init__(self, missing_permissions: List[str]): ...
missing_permissions: List[str]
class NSFWChannelRequired(CheckFailure):
"""Command requires NSFW channel."""
def __init__(self, channel: GuildChannel): ...
channel: GuildChannel
# Cooldown errors
class CommandOnCooldown(CommandError):
"""Command is on cooldown."""
def __init__(self, cooldown: Cooldown, retry_after: float, type: BucketType): ...
cooldown: Cooldown
retry_after: float
type: BucketType
class MaxConcurrencyReached(CommandError):
"""Maximum concurrent executions reached."""
def __init__(self, number: int, per: BucketType): ...
number: int
per: BucketType
# Conversion errors
class MemberNotFound(BadArgument):
"""Member could not be found."""
def __init__(self, argument: str): ...
argument: str
class UserNotFound(BadArgument):
"""User could not be found."""
def __init__(self, argument: str): ...
argument: str
class MessageNotFound(BadArgument):
"""Message could not be found."""
def __init__(self, argument: str): ...
argument: str
class ChannelNotFound(BadArgument):
"""Channel could not be found."""
def __init__(self, argument: str): ...
argument: str
class ChannelNotReadable(BadArgument):
"""Channel is not readable by the bot."""
def __init__(self, argument: Union[GuildChannel, str]): ...
argument: Union[GuildChannel, str]
class RoleNotFound(BadArgument):
"""Role could not be found."""
def __init__(self, argument: str): ...
argument: str
class EmojiNotFound(BadArgument):
"""Emoji could not be found."""
def __init__(self, argument: str): ...
argument: str
class GuildNotFound(BadArgument):
"""Guild could not be found."""
def __init__(self, argument: str): ...
argument: str
class BadInviteArgument(BadArgument):
"""Invalid invite argument."""
pass
class BadColourArgument(BadArgument):
"""Invalid colour argument."""
def __init__(self, argument: str): ...
argument: str
BadColorArgument = BadColourArgument # AliasCustomizable help command system with built-in implementations and support for custom help formats.
class HelpCommand:
"""
Base help command implementation.
"""
def __init__(self, **options): ...
context: Optional[Context] # Current command context
verify_checks: bool # Whether to verify command checks
command_attrs: Dict[str, Any] # Attributes for help command
def copy(self) -> HelpCommand:
"""Create a copy of the help command."""
async def prepare_help_command(self, ctx: Context, command: Optional[Command] = None) -> None:
"""Prepare help command for execution."""
async def command_callback(self, ctx: Context, *, command: str = None) -> None:
"""Help command callback."""
def get_bot_mapping(self) -> Dict[Optional[Cog], List[Command]]:
"""Get mapping of cogs to commands."""
def filter_commands(self, commands: List[Command], *, sort: bool = False, key: Callable = None) -> List[Command]:
"""Filter commands based on checks."""
def get_max_size(self, commands: List[Command]) -> int:
"""Get maximum command name size."""
def get_command_signature(self, command: Command) -> str:
"""Get command signature string."""
def get_opening_note(self) -> str:
"""Get opening note for help."""
def get_ending_note(self) -> str:
"""Get ending note for help."""
def add_indented_commands(self, commands: List[Command], *, heading: str, max_size: int = None) -> None:
"""Add indented command list."""
# Subclass hooks
async def send_bot_help(self, mapping: Dict[Optional[Cog], List[Command]]) -> None:
"""Send bot help (all commands)."""
async def send_cog_help(self, cog: Cog) -> None:
"""Send help for a specific cog."""
async def send_group_help(self, group: Group) -> None:
"""Send help for a command group."""
async def send_command_help(self, command: Command) -> None:
"""Send help for a specific command."""
async def send_error_message(self, error: str) -> None:
"""Send error message."""
async def on_help_command_error(self, ctx: Context, error: CommandError) -> None:
"""Handle help command errors."""
def command_not_found(self, string: str) -> str:
"""Return message for command not found."""
def subcommand_not_found(self, command: Command, string: str) -> str:
"""Return message for subcommand not found."""
class DefaultHelpCommand(HelpCommand):
"""
Default help command implementation with embed formatting.
"""
def __init__(self, **options): ...
# Formatting options
no_category: str # Name for uncategorized commands
paginator: Paginator # Text paginator
sort_commands: bool # Whether to sort commands
dm_help: Optional[bool] # Whether to DM help
dm_help_threshold: Optional[int] # Member count threshold for DM
class MinimalHelpCommand(HelpCommand):
"""
Minimal help command implementation.
"""
def __init__(self, **options): ...
# Simplified help output
sort_commands: bool
commands_heading: str
aliases_heading: str
dm_help: Optional[bool]
dm_help_threshold: Optional[int]
no_category: str
class Paginator:
"""
Text paginator for breaking long help output into pages.
"""
def __init__(self, prefix: str = '```', suffix: str = '```', max_size: int = 2000, linesep: str = '\n'): ...
prefix: str # Page prefix
suffix: str # Page suffix
max_size: int # Maximum page size
linesep: str # Line separator
def add_line(self, line: str = '', *, empty: bool = False) -> None:
"""Add a line to the paginator."""
def close_page(self) -> None:
"""Close current page."""
@property
def pages(self) -> List[str]:
"""Get all pages."""import discord
from discord.ext import commands
bot = commands.Bot(command_prefix='!', intents=discord.Intents.default())
@bot.event
async def on_ready():
print(f'Bot is ready! Logged in as {bot.user}')
@bot.command()
async def ping(ctx):
await ctx.send('Pong!')
@bot.command()
async def echo(ctx, *, message):
await ctx.send(message)
bot.run('YOUR_TOKEN')@bot.command()
@commands.has_permissions(manage_messages=True)
@commands.cooldown(1, 30, commands.BucketType.guild)
async def purge(ctx, amount: int):
if amount > 100:
await ctx.send("Cannot delete more than 100 messages at once.")
return
deleted = await ctx.channel.purge(limit=amount + 1) # +1 for command message
await ctx.send(f"Deleted {len(deleted) - 1} messages.", delete_after=5)class ModerationCog(commands.Cog, name="Moderation"):
def __init__(self, bot):
self.bot = bot
@commands.command()
@commands.has_permissions(kick_members=True)
async def kick(self, ctx, member: discord.Member, *, reason=None):
await member.kick(reason=reason)
await ctx.send(f'{member} has been kicked.')
@commands.command()
@commands.has_permissions(ban_members=True)
async def ban(self, ctx, member: discord.Member, *, reason=None):
await member.ban(reason=reason)
await ctx.send(f'{member} has been banned.')
async def setup(bot):
await bot.add_cog(ModerationCog(bot))Install with Tessl CLI
npx tessl i tessl/pypi-discord-py