diff --git a/command_context.py b/command_context.py new file mode 100644 index 0000000..b780a1d --- /dev/null +++ b/command_context.py @@ -0,0 +1,122 @@ +""" +Utility module for handling command contexts in Discord.py. + +This module provides functions for setting up commands with different contexts, +allowing them to work in DMs, private channels, guilds, or any combination. +""" + +import discord +from discord import app_commands +from enum import Enum, auto +from typing import Optional, List, Dict, Any, Union, Callable, Awaitable + +class AppCommandContext(Enum): + """ + Enum representing the allowed contexts for application commands. + + This enum defines where commands can be used: + - GUILD: Commands can only be used in guilds (servers) + - GUILD_INSTALL: Commands can only be used in guilds where the app is installed + - PRIVATE: Commands can only be used in private contexts (DMs and private channels) + - ALL: Commands can be used in both guilds and private contexts + """ + GUILD = auto() + GUILD_INSTALL = auto() + PRIVATE = auto() + ALL = auto() + +def set_command_context( + command: app_commands.Command, + context: AppCommandContext = AppCommandContext.ALL +) -> app_commands.Command: + """ + Set the context for a command, determining where it can be used. + + Args: + command: The command to modify + context: The context to set for the command + + Returns: + The modified command + """ + # Set guild_only and dm_permission based on the context + if context == AppCommandContext.GUILD or context == AppCommandContext.GUILD_INSTALL: + command.guild_only = True + # dm_permission is automatically set to False when guild_only is True + elif context == AppCommandContext.PRIVATE: + command.guild_only = False + # We need to override the extras to set dm_permission to True and guild_permission to False + command.extras["dm_permission"] = True + command.extras["default_member_permissions"] = "0" # No permissions in guild + elif context == AppCommandContext.ALL: + command.guild_only = False + command.extras["dm_permission"] = True + # Allow default permissions in guilds + + return command + +def create_global_command( + bot_tree: app_commands.CommandTree, + name: str, + description: str, + callback: Callable[[discord.Interaction], Awaitable[None]], + context: AppCommandContext = AppCommandContext.ALL, + **kwargs +) -> app_commands.Command: + """ + Create a command that can be used globally with the specified context. + + Args: + bot_tree: The command tree to add the command to + name: The name of the command + description: The description of the command + callback: The function to call when the command is used + context: The context to set for the command + **kwargs: Additional arguments to pass to the command constructor + + Returns: + The created command + """ + # Create the command + command = app_commands.Command( + name=name, + description=description, + callback=callback, + **kwargs + ) + + # Set the context + set_command_context(command, context) + + # Add the command to the tree + bot_tree.add_command(command) + + return command + +def sync_commands( + bot_tree: app_commands.CommandTree, + guild_specific: bool = True, + global_commands: bool = True +) -> Awaitable[List[app_commands.Command]]: + """ + Sync commands with Discord. + + Args: + bot_tree: The command tree to sync + guild_specific: Whether to sync guild-specific commands + global_commands: Whether to sync global commands + + Returns: + A coroutine that resolves to the list of synced commands + """ + if guild_specific and not global_commands: + # Only sync guild-specific commands + # This is handled by command_customization.register_all_guild_commands + return None + elif not guild_specific and global_commands: + # Only sync global commands + return bot_tree.sync() + else: + # Sync both guild-specific and global commands + # This is the default behavior of bot_tree.sync() + return bot_tree.sync() diff --git a/command_sync_utils.py b/command_sync_utils.py new file mode 100644 index 0000000..840f37e --- /dev/null +++ b/command_sync_utils.py @@ -0,0 +1,120 @@ +""" +Utility module for syncing commands with Discord. + +This module provides functions for syncing both global and guild-specific commands, +allowing for more flexibility in command registration and supporting commands in +both guild and private contexts (DMs and private channels). +""" + +import discord +from discord import app_commands +from discord.ext import commands +import logging +import command_customization +from typing import Dict, List, Optional, Union, Any +from command_context import AppCommandContext + +# Configure logging +logging.basicConfig(level=logging.INFO) +log = logging.getLogger(__name__) + +async def sync_global_and_guild_commands( + bot: commands.Bot, + global_only: bool = False, + guild_only: bool = False +) -> Dict[str, Any]: + """ + Sync both global and guild-specific commands with Discord. + + This function allows for more control over command syncing, enabling + both global commands (that work in DMs) and guild-specific commands + with customizations. + + Args: + bot: The bot instance + global_only: If True, only sync global commands + guild_only: If True, only sync guild-specific commands + + Returns: + A dictionary containing the results of the sync operations + """ + results = { + "global": [], + "guild": {} + } + + try: + # Sync global commands if requested + if not guild_only: + log.info("Syncing global commands...") + global_synced = await bot.tree.sync() + results["global"] = global_synced + log.info(f"Synced {len(global_synced)} global command(s)") + + # List the synced commands + global_commands = [cmd.name for cmd in global_synced] + log.info(f"Global commands: {', '.join(global_commands)}") + + # Sync guild-specific commands if requested + if not global_only: + log.info("Syncing guild-specific command customizations...") + guild_syncs = await command_customization.register_all_guild_commands(bot) + results["guild"] = guild_syncs + + total_guild_syncs = sum(len(cmds) for cmds in guild_syncs.values()) + log.info(f"Synced commands for {len(guild_syncs)} guilds with a total of {total_guild_syncs} customized commands") + + except Exception as e: + log.error(f"Failed to sync commands: {e}") + import traceback + traceback.print_exc() + + return results + +def register_global_command( + bot: commands.Bot, + name: str, + description: str, + callback: Any, + context: AppCommandContext = AppCommandContext.ALL, + **kwargs +) -> app_commands.Command: + """ + Register a global command with the specified context. + + Args: + bot: The bot instance + name: The name of the command + description: The description of the command + callback: The function to call when the command is used + context: The context to set for the command + **kwargs: Additional arguments to pass to the command constructor + + Returns: + The created command + """ + from command_context import create_global_command + + return create_global_command( + bot.tree, + name=name, + description=description, + callback=callback, + context=context, + **kwargs + ) + +def set_command_contexts(bot: commands.Bot, contexts: Dict[str, AppCommandContext]) -> None: + """ + Set the contexts for multiple commands. + + Args: + bot: The bot instance + contexts: A dictionary mapping command names to contexts + """ + from command_context import set_command_context + + for cmd in bot.tree.get_commands(): + if cmd.name in contexts: + set_command_context(cmd, contexts[cmd.name]) + log.info(f"Set context for command '{cmd.name}' to {contexts[cmd.name]}") diff --git a/example_global_commands.py b/example_global_commands.py new file mode 100644 index 0000000..1e43384 --- /dev/null +++ b/example_global_commands.py @@ -0,0 +1,97 @@ +""" +Example module demonstrating how to create global commands that work in DMs and private channels. + +This module shows how to use the AppCommandContext system to create commands +that can be used in both guild and private contexts. +""" + +import discord +from discord import app_commands +from discord.ext import commands +from command_context import AppCommandContext, create_global_command +from typing import Optional + +class GlobalCommandsCog(commands.Cog): + """A cog that demonstrates global commands that work in DMs and private channels.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + print("GlobalCommandsCog initialized!") + + # Register global commands + self.register_global_commands() + + def register_global_commands(self): + """Register global commands that work in DMs and private channels.""" + + # Create a help command that works in DMs and private channels + create_global_command( + self.bot.tree, + name="dmhelp", + description="Get help with bot commands (works in DMs)", + callback=self.dm_help_callback, + context=AppCommandContext.ALL + ) + + # Create a ping command that works in DMs and private channels + create_global_command( + self.bot.tree, + name="dmping", + description="Check if the bot is responsive (works in DMs)", + callback=self.dm_ping_callback, + context=AppCommandContext.ALL + ) + + # Create a command that only works in private contexts + create_global_command( + self.bot.tree, + name="privateonly", + description="This command only works in DMs and private channels", + callback=self.private_only_callback, + context=AppCommandContext.PRIVATE + ) + + print("GlobalCommandsCog: Registered global commands") + + async def dm_help_callback(self, interaction: discord.Interaction): + """Callback for the /dmhelp command.""" + is_dm = isinstance(interaction.channel, discord.DMChannel) + is_private = not interaction.guild + + help_text = ( + "# Bot Help\n\n" + "This help command works in both DMs and servers!\n\n" + f"**Current context:** {'DM' if is_dm else 'Private Channel' if is_private else 'Server'}\n\n" + "## Available Commands\n" + "- `/dmhelp` - This help command\n" + "- `/dmping` - Check if the bot is responsive\n" + "- `/privateonly` - Only works in DMs and private channels\n" + ) + + await interaction.response.send_message(help_text, ephemeral=True) + + async def dm_ping_callback(self, interaction: discord.Interaction): + """Callback for the /dmping command.""" + is_dm = isinstance(interaction.channel, discord.DMChannel) + is_private = not interaction.guild + + await interaction.response.send_message( + f"🏓 Pong! Bot is responsive.\n" + f"**Current context:** {'DM' if is_dm else 'Private Channel' if is_private else 'Server'}", + ephemeral=True + ) + + async def private_only_callback(self, interaction: discord.Interaction): + """Callback for the /privateonly command.""" + is_dm = isinstance(interaction.channel, discord.DMChannel) + + await interaction.response.send_message( + f"This command only works in private contexts.\n" + f"**Current context:** {'DM' if is_dm else 'Private Channel'}", + ephemeral=True + ) + +async def setup(bot: commands.Bot): + """Add the GlobalCommandsCog to the bot.""" + await bot.add_cog(GlobalCommandsCog(bot)) + print("GlobalCommandsCog setup complete.") diff --git a/gurt_bot.py b/gurt_bot.py index 8878ef5..157fa2a 100644 --- a/gurt_bot.py +++ b/gurt_bot.py @@ -28,8 +28,43 @@ async def on_ready(): # Sync commands try: print("Starting command sync process...") - synced = await bot.tree.sync() - print(f"Synced {len(synced)} command(s)") + + # Import our command sync utilities + try: + import command_sync_utils + from command_context import AppCommandContext + + # Sync both global and guild-specific commands + print("Syncing both global and guild-specific commands...") + sync_results = await command_sync_utils.sync_global_and_guild_commands(bot) + + # Report results + global_syncs = sync_results["global"] + guild_syncs = sync_results["guild"] + + print(f"Synced {len(global_syncs)} global command(s)") + if guild_syncs: + total_guild_syncs = sum(len(cmds) for cmds in guild_syncs.values()) + print(f"Synced commands for {len(guild_syncs)} guilds with a total of {total_guild_syncs} customized commands") + + # Set context for specific commands to allow them in private channels and DMs + private_commands = [ + # Add command names that should work in private contexts here + "gurtmood", # Allow mood command in DMs + ] + + # Create a dictionary mapping command names to contexts + command_contexts = {name: AppCommandContext.ALL for name in private_commands} + + # Apply the contexts to the commands + command_sync_utils.set_command_contexts(bot, command_contexts) + + except ImportError: + # Fall back to regular sync if the utility modules aren't available + print("Command sync utilities not available, falling back to regular sync...") + synced = await bot.tree.sync() + print(f"Synced {len(synced)} command(s)") + except Exception as e: print(f"Failed to sync commands: {e}") import traceback diff --git a/main.py b/main.py index 357b698..3073874 100644 --- a/main.py +++ b/main.py @@ -230,19 +230,46 @@ async def on_ready(): commands_before = [cmd.name for cmd in bot.tree.get_commands()] print(f"Commands before sync: {commands_before}") - # Skip global command sync to avoid duplication - print("Skipping global command sync to avoid command duplication...") + # Import our new command sync utilities + import command_sync_utils + from command_context import AppCommandContext - # Only sync guild-specific commands with customizations - print("Syncing guild-specific command customizations...") - guild_syncs = await command_customization.register_all_guild_commands(bot) + # Sync both global and guild-specific commands + print("Syncing both global and guild-specific commands...") + sync_results = await command_sync_utils.sync_global_and_guild_commands(bot) + # Report results + global_syncs = sync_results["global"] + guild_syncs = sync_results["guild"] + + print(f"Synced {len(global_syncs)} global command(s)") total_guild_syncs = sum(len(cmds) for cmds in guild_syncs.values()) print(f"Synced commands for {len(guild_syncs)} guilds with a total of {total_guild_syncs} customized commands") # List commands after sync commands_after = [cmd.name for cmd in bot.tree.get_commands()] - print(f"Commands registered in command tree: {commands_after}") + print(f"Commands registered in command tree: {', '.join(commands_after)}") + + # Set context for specific commands to allow them in private channels and DMs + private_commands = [ + # Add command names that should work in private contexts here + "dmhelp", + "dmping", + "privateonly", + ] + + # Load the example global commands cog + try: + await bot.load_extension("example_global_commands") + print("Loaded example_global_commands cog") + except Exception as e: + print(f"Failed to load example_global_commands cog: {e}") + + # Create a dictionary mapping command names to contexts + command_contexts = {name: AppCommandContext.ALL for name in private_commands} + + # Apply the contexts to the commands + command_sync_utils.set_command_contexts(bot, command_contexts) except Exception as e: print(f"Failed to sync commands: {e}") diff --git a/wheatley_bot.py b/wheatley_bot.py index 5e1b526..7ef8311 100644 --- a/wheatley_bot.py +++ b/wheatley_bot.py @@ -28,8 +28,43 @@ async def on_ready(): # Sync commands try: print("Starting command sync process...") - synced = await bot.tree.sync() - print(f"Synced {len(synced)} command(s)") + + # Import our command sync utilities + try: + import command_sync_utils + from command_context import AppCommandContext + + # Sync both global and guild-specific commands + print("Syncing both global and guild-specific commands...") + sync_results = await command_sync_utils.sync_global_and_guild_commands(bot) + + # Report results + global_syncs = sync_results["global"] + guild_syncs = sync_results["guild"] + + print(f"Synced {len(global_syncs)} global command(s)") + if guild_syncs: + total_guild_syncs = sum(len(cmds) for cmds in guild_syncs.values()) + print(f"Synced commands for {len(guild_syncs)} guilds with a total of {total_guild_syncs} customized commands") + + # Set context for specific commands to allow them in private channels and DMs + private_commands = [ + # Add command names that should work in private contexts here + "wheatleymemory", # Allow memory command in DMs + ] + + # Create a dictionary mapping command names to contexts + command_contexts = {name: AppCommandContext.ALL for name in private_commands} + + # Apply the contexts to the commands + command_sync_utils.set_command_contexts(bot, command_contexts) + + except ImportError: + # Fall back to regular sync if the utility modules aren't available + print("Command sync utilities not available, falling back to regular sync...") + synced = await bot.tree.sync() + print(f"Synced {len(synced)} command(s)") + except Exception as e: print(f"Failed to sync commands: {e}") import traceback