Skip to content

Instantly share code, notes, and snippets.

@Soheab
Last active March 22, 2024 22:35
Show Gist options
  • Save Soheab/fed903c25b1aae1f11a8ca8c33243131 to your computer and use it in GitHub Desktop.
Save Soheab/fed903c25b1aae1f11a8ca8c33243131 to your computer and use it in GitHub Desktop.
Here is how to store app_commands.AppCommand to access later for mentioning or anything.

AppCommand store

Here is how to store app_commands.AppCommand to access later for mentioning or anything.

There are two options here:
1. Subclass commands.Bot and add some methods and attributes
2. Subclass app_commands.CommandTree (recommended)

Subclassing commands.Bot

Here is how to use option 1

Code: here

You can either copypaste the whole paste into your code and use it OR read over the code and copy the parts you need into your own code. Using it as easy as doing the following:

bot = MyBot(command_prefix=..., intents=...)

You can change the class to subclass discord.Client instead if you want. After defining an instance, you can start implementing the methods it your code.

A sync command would look something the following:

# define our tree
tree = bot.tree
# sync and assign a var to get the return
commands = await tree.sync(guild=...)
# call our custom method to cache the commands
await bot.update_app_commands_cache(commands, guild=...)

What about mentioning?! That would be something like the following:

# need to mention a global command called "ping"?
command = bot.get_app_command("ping")
# or a guild command named "foo"
command = bot.get_app_command("foo", guild=guild_it_belongs_to)
# subcommands? yes!
# guild kwarg is optional if the group is global
command = bot.get_app_command("full name", guild=...)
await ...send(command.mention)

You can also call update_app_commands_cache without any commands, it will fetch all commands in that case.

Subclassing CommandTree

Here is how to use option 2

Code: here

You can either copypaste the whole paste into your code and use it OR read over the code and copy the parts you need into your own code. Using it as easy as doing the following:

bot = commands.Bot(command_prefix=..., intents=..., tree_cls=MyCommandTree)

# using discord.Client? that would be:
client = discord.Client(intents=...)
client.tree = MyCommandTree(client)

A sync command would look something the following:

# define our tree
tree = client/bot.tree
await tree.sync(guild=...)

What about mentioning?! That would be something like the following:

# need to mention a global command called "ping"?
command = client/bot.tree.get_app_command("ping")
# or a guild command named "foo"
command = client/bot.tree.get_app_command("foo", guild=guild_it_belongs_to)
# subcommands? yes!
# guild kwarg is optional if the group is global
command = client/bot.tree.get_app_command("full name", guild=...)
await ...send(command.mention)

The provided MyCommandTree subclass will automatically cache all app commands if you used the following methods: .sync, .fetch_command and .fetch_commands. .clear_commands will also clear the cache commands (per scope)

Extra

These are for both options

  1. There is a method called find_app_command_by_names it's for kinda fuzzysearching through your commands to find the right one. It's used like this:
 res = <bot>(.tree?).find_app_command_by_names("ping", "foo", "bar", guild=...)

it will try to search through all commands (including subcommands) to find one command with the passed names.

  1. get_app_command also supports getting the AppCommand from its ID if you got that somehow.
from __future__ import annotations
from typing import Dict, Optional, List, TYPE_CHECKING, Union
import discord
from discord import app_commands
from discord.ext import commands
if TYPE_CHECKING:
from discord.abc import Snowflake
AppCommandStore = Dict[str, app_commands.AppCommand] # name: AppCommand
class MyBot(commands.Bot):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._global_app_commands: AppCommandStore = {}
# guild_id: AppCommandStore
self._guild_app_commands: Dict[int, AppCommandStore] = {}
def find_app_command_by_names(
self,
*qualified_name: str,
guild: Optional[Union[Snowflake, int]] = None,
fallback_to_global: bool = True,
) -> Optional[app_commands.AppCommand]:
commands = self._global_app_commands
if guild:
guild_id = guild.id if not isinstance(guild, int) else guild
guild_commands = self._guild_app_commands.get(guild_id, {})
if not guild_commands and fallback_to_global:
commands = self._global_app_commands
else:
commands = guild_commands
for cmd_name, cmd in commands.items():
if any(name in qualified_name for name in cmd_name.split()):
return cmd
return None
def get_app_command(
self,
value: Union[str, int],
guild: Optional[Union[Snowflake, int]] = None,
fallback_to_global: bool = True,
) -> Optional[app_commands.AppCommand]:
def search_dict(d: AppCommandStore) -> Optional[app_commands.AppCommand]:
for cmd_name, cmd in d.items():
if value == cmd_name or (str(value).isdigit() and int(value) == cmd.id):
return cmd
return None
if guild:
guild_id = guild.id if not isinstance(guild, int) else guild
guild_commands = self._guild_app_commands.get(guild_id, {})
if not fallback_to_global:
return search_dict(guild_commands)
else:
return search_dict(guild_commands) or search_dict(self._global_app_commands)
else:
return search_dict(self._global_app_commands)
async def update_app_commands_cache(
self,
commands: Optional[List[app_commands.AppCommand]] = None,
guild: Optional[Union[Snowflake, int]] = None
) -> None:
def unpack_app_commands(commands: List[app_commands.AppCommand]) -> AppCommandStore:
ret: AppCommandStore = {}
def unpack_options(options: List[Union[app_commands.AppCommand, app_commands.AppCommandGroup, app_commands.Argument]]):
for option in options:
if isinstance(option, app_commands.AppCommandGroup):
ret[option.qualified_name] = option # type: ignore
unpack_options(option.options) # type: ignore
for command in commands:
ret[command.name] = command
unpack_options(command.options) # type: ignore
return ret
# because we support both int and Snowflake
# we need to convert it to a Snowflake like object if it's an int
_guild: Optional[Snowflake] = None
if guild is not None:
if isinstance(guild, int):
_guild = discord.Object(guild)
else:
_guild = guild
tree: app_commands.CommandTree = self.tree
# commands.Bot has a built-in tree
# this should be point to your tree if using discord.Client
if not commands:
commands = await tree.fetch_commands(guild=_guild)
if _guild:
self._guild_app_commands[_guild.id] = unpack_app_commands(commands)
else:
self._global_app_commands = unpack_app_commands(commands)
from __future__ import annotations
from typing import Dict, Optional, List, TYPE_CHECKING, Union
import discord
from discord import app_commands
if TYPE_CHECKING:
from discord.abc import Snowflake
AppCommandStore = Dict[str, app_commands.AppCommand] # name: AppCommand
class MyCommandTree(app_commands.CommandTree):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._global_app_commands: AppCommandStore = {}
# guild_id: AppCommandStore
self._guild_app_commands: Dict[int, AppCommandStore] = {}
def find_app_command_by_names(
self,
*qualified_name: str,
guild: Optional[Union[Snowflake, int]] = None,
) -> Optional[app_commands.AppCommand]:
commands = self._global_app_commands
if guild:
guild_id = guild.id if not isinstance(guild, int) else guild
guild_commands = self._guild_app_commands.get(guild_id, {})
if not guild_commands and self.fallback_to_global:
commands = self._global_app_commands
else:
commands = guild_commands
for cmd_name, cmd in commands.items():
if any(name in qualified_name for name in cmd_name.split()):
return cmd
return None
def get_app_command(
self,
value: Union[str, int],
guild: Optional[Union[Snowflake, int]] = None,
) -> Optional[app_commands.AppCommand]:
def search_dict(d: AppCommandStore) -> Optional[app_commands.AppCommand]:
for cmd_name, cmd in d.items():
if value == cmd_name or (str(value).isdigit() and int(value) == cmd.id):
return cmd
return None
if guild:
guild_id = guild.id if not isinstance(guild, int) else guild
guild_commands = self._guild_app_commands.get(guild_id, {})
if not self.fallback_to_global:
return search_dict(guild_commands)
else:
return search_dict(guild_commands) or search_dict(self._global_app_commands)
else:
return search_dict(self._global_app_commands)
@staticmethod
def _unpack_app_commands(commands: List[app_commands.AppCommand]) -> AppCommandStore:
ret: AppCommandStore = {}
def unpack_options(options: List[Union[app_commands.AppCommand, app_commands.AppCommandGroup, app_commands.Argument]]):
for option in options:
if isinstance(option, app_commands.AppCommandGroup):
ret[option.qualified_name] = option # type: ignore
unpack_options(option.options) # type: ignore
for command in commands:
ret[command.name] = command
unpack_options(command.options) # type: ignore
return ret
async def _update_cache(
self,
commands: List[app_commands.AppCommand],
guild: Optional[Union[Snowflake, int]] = None
) -> None:
# because we support both int and Snowflake
# we need to convert it to a Snowflake like object if it's an int
_guild: Optional[Snowflake] = None
if guild is not None:
if isinstance(guild, int):
_guild = discord.Object(guild)
else:
_guild = guild
if _guild:
self._guild_app_commands[_guild.id] = self._unpack_app_commands(commands)
else:
self._global_app_commands = self._unpack_app_commands(commands)
async def fetch_command(self, command_id: int, /, *, guild: Optional[Snowflake] = None) -> app_commands.AppCommand:
res = await super().fetch_command(command_id, guild=guild)
await self._update_cache([res], guild=guild)
return res
async def fetch_commands(self, *, guild: Optional[Snowflake] = None) -> List[app_commands.AppCommand]:
res = await super().fetch_commands(guild=guild)
await self._update_cache(res, guild=guild)
return res
def clear_app_commands_cache(self, *, guild: Optional[Snowflake]) -> None:
if guild:
self._guild_app_commands.pop(guild.id, None)
else:
self._global_app_commands = {}
def clear_commands(self, *, guild: Optional[Snowflake], type: Optional[discord.AppCommandType] = None, clear_app_commands_cache: bool = True) -> None:
super().clear_commands(guild=guild)
if clear_app_commands_cache:
self.clear_app_commands_cache(guild=guild)
async def sync(self, *, guild: Optional[Snowflake] = None) -> List[app_commands.AppCommand]:
res = await super().sync(guild=guild)
await self._update_cache(res, guild=guild)
return res
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment