This commit is contained in:
Slipstream 2025-05-13 08:50:18 -06:00
parent 4336e4174b
commit af5023477c
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
7 changed files with 13 additions and 512 deletions

View File

@ -1,131 +0,0 @@
"""
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:
# Guild-only commands
command.guild_only = True
# dm_permission is automatically set to False when guild_only is True
elif context == AppCommandContext.PRIVATE:
# Private-only commands (DMs and private channels)
command.guild_only = False
# Set dm_permission to True
if not hasattr(command, "extras"):
command.extras = {}
command.extras["dm_permission"] = True
# Disable in guilds by setting default_member_permissions to "0"
command.extras["default_member_permissions"] = "0"
elif context == AppCommandContext.ALL:
# Commands that work everywhere
command.guild_only = False
# Set dm_permission to True
if not hasattr(command, "extras"):
command.extras = {}
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()

View File

@ -203,26 +203,17 @@ async def register_guild_commands(bot, guild: discord.Guild) -> List[app_command
return await syncer.sync_guild_commands(guild)
async def register_all_guild_commands(bot, global_command_names=None) -> Dict[int, List[app_commands.Command]]:
async def register_all_guild_commands(bot) -> Dict[int, List[app_commands.Command]]:
"""
Register commands for all guilds with customizations.
Returns a dictionary mapping guild IDs to lists of registered commands.
Args:
bot: The bot instance
global_command_names: Optional list of command names that are already registered globally
(not used in this implementation, kept for compatibility)
"""
syncer = GuildCommandSyncer(bot)
results = {}
for guild in bot.guilds:
try:
# Sync all commands to this guild
log.info(f"Syncing commands for guild {guild.id}")
results[guild.id] = await syncer.sync_guild_commands(guild)
log.info(f"Synced {len(results[guild.id])} commands for guild {guild.id}")
except Exception as e:
log.error(f"Failed to sync commands for guild {guild.id}: {e}")
results[guild.id] = []

View File

@ -1,156 +0,0 @@
"""
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,
private_command_names: List[str] = None
) -> Dict[str, Any]:
"""
Sync commands with Discord, first to guilds and then globally.
This function implements the following strategy:
1. First, sync guild-specific commands to every guild (as before)
2. Then, sync global commands with dm_permission=True and guild_only=False
Args:
bot: The bot instance
global_only: If True, only sync global commands
guild_only: If True, only sync guild-specific commands
private_command_names: List of command names that should work in private contexts
Returns:
A dictionary containing the results of the sync operations
"""
results = {
"global": [],
"guild": {}
}
try:
# Get all commands from the command tree
all_commands = bot.tree.get_commands()
command_names = [cmd.name for cmd in all_commands]
log.info(f"Found {len(all_commands)} commands in tree: {', '.join(command_names)}")
# If private_command_names is not provided, use an empty list
if private_command_names is None:
private_command_names = []
# First, sync guild-specific commands if requested
if not global_only:
log.info("Syncing guild-specific commands to all guilds...")
# Use the command_customization module to handle guild-specific commands
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")
# Now, prepare global commands with appropriate context settings
if not guild_only and private_command_names:
log.info("Preparing global commands for private contexts...")
# Clear the command tree for global commands
bot.tree.clear_commands()
# Add back only the commands that should work in private contexts
from command_context import AppCommandContext, set_command_context
for cmd_name in private_command_names:
# Find the command in the original list
cmd = next((c for c in all_commands if c.name == cmd_name), None)
if cmd:
log.info(f"Setting command {cmd_name} to work in private contexts")
# Set the command to work in private contexts
set_command_context(cmd, AppCommandContext.ALL)
# Add the command back to the tree
bot.tree.add_command(cmd)
else:
log.warning(f"Command {cmd_name} not found in command tree")
# Sync the global commands
log.info("Syncing global commands for private contexts...")
global_synced = await bot.tree.sync()
results["global"] = global_synced
# List the synced commands
global_command_names = [cmd.name for cmd in global_synced]
log.info(f"Synced {len(global_synced)} global command(s) for private contexts: {', '.join(global_command_names)}")
# Restore the original command tree
bot.tree.clear_commands()
for cmd in all_commands:
bot.tree.add_command(cmd)
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]}")

View File

@ -1,97 +0,0 @@
"""
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.")

View File

@ -28,42 +28,8 @@ async def on_ready():
# Sync commands
try:
print("Starting command sync process...")
# Import our command sync utilities
try:
import command_sync_utils
# Define commands that should work in private channels and DMs
private_commands = [
# Add command names that should work in private contexts here
"gurtmood", # Allow mood command in DMs
]
# First sync guild-specific commands to all guilds, then sync global commands for private contexts
print("First syncing commands to all guilds, then syncing global commands for private contexts...")
sync_results = await command_sync_utils.sync_global_and_guild_commands(
bot,
private_command_names=private_commands
)
# Report results
global_syncs = sync_results["global"]
guild_syncs = sync_results["guild"]
# Report guild syncs
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} commands")
# Report global syncs
print(f"Synced {len(global_syncs)} global command(s) for private 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)")
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

54
main.py
View File

@ -230,57 +230,19 @@ async def on_ready():
commands_before = [cmd.name for cmd in bot.tree.get_commands()]
print(f"Commands before sync: {commands_before}")
# Import our new command sync utilities
import command_sync_utils
# Skip global command sync to avoid duplication
print("Skipping global command sync to avoid command duplication...")
# First sync guild-specific commands to all guilds, then sync global commands for private contexts
print("First syncing commands to all guilds, then syncing global commands for private contexts...")
sync_results = await command_sync_utils.sync_global_and_guild_commands(
bot,
private_command_names=private_commands
)
# Only sync guild-specific commands with customizations
print("Syncing guild-specific command customizations...")
guild_syncs = await command_customization.register_all_guild_commands(bot)
# Report results
global_syncs = sync_results["global"]
guild_syncs = sync_results["guild"]
# Report guild syncs
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} commands")
# Report global syncs
print(f"Synced {len(global_syncs)} global command(s) for private contexts")
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: {', '.join(commands_after)}")
# Define commands that should work 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}")
# Sync both global and guild-specific commands, avoiding duplicates
print("Re-syncing commands with private context settings...")
sync_results = await command_sync_utils.sync_global_and_guild_commands(
bot,
private_command_names=private_commands
)
# Report results
global_syncs = sync_results["global"]
print(f"Re-synced {len(global_syncs)} global command(s) with private context settings")
print(f"Commands registered in command tree: {commands_after}")
except Exception as e:
print(f"Failed to sync commands: {e}")

View File

@ -28,42 +28,8 @@ async def on_ready():
# Sync commands
try:
print("Starting command sync process...")
# Import our command sync utilities
try:
import command_sync_utils
# Define commands that should work in private channels and DMs
private_commands = [
# Add command names that should work in private contexts here
"wheatleymemory", # Allow memory command in DMs
]
# First sync guild-specific commands to all guilds, then sync global commands for private contexts
print("First syncing commands to all guilds, then syncing global commands for private contexts...")
sync_results = await command_sync_utils.sync_global_and_guild_commands(
bot,
private_command_names=private_commands
)
# Report results
global_syncs = sync_results["global"]
guild_syncs = sync_results["guild"]
# Report guild syncs
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} commands")
# Report global syncs
print(f"Synced {len(global_syncs)} global command(s) for private 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)")
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