The /cc-create command was immediately creating the custom command after the modal was submitted, instead of waiting for the user to click the "Create Command" button in the confirmation view. Issue: Command handler was calling create_command() service immediately after modal submission, before user confirmed via the preview buttons. Fix: Removed premature command creation logic from the command handler. The modal's on_submit method already shows a preview with confirmation buttons, and the CustomCommandCreateConfirmationView.confirm_create button handler properly creates the command only when the user clicks "Create Command". Flow now correctly: 1. User submits modal with command details 2. Preview displays with "Create Command" and "Cancel" buttons 3. Command is only created when user clicks "Create Command" button 4. User can cancel without creating anything 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
578 lines
23 KiB
Python
578 lines
23 KiB
Python
"""
|
|
Custom Commands slash commands for Discord Bot v2.0
|
|
|
|
Modern implementation with interactive views and excellent UX.
|
|
"""
|
|
from typing import Optional, List
|
|
import discord
|
|
from discord import app_commands
|
|
from discord.ext import commands
|
|
|
|
from services.custom_commands_service import (
|
|
custom_commands_service,
|
|
CustomCommandNotFoundError,
|
|
CustomCommandExistsError,
|
|
CustomCommandPermissionError
|
|
)
|
|
from models.custom_command import CustomCommandSearchFilters
|
|
from utils.logging import get_contextual_logger
|
|
from utils.decorators import logged_command
|
|
from utils.permissions import requires_team
|
|
from views.embeds import EmbedTemplate, EmbedColors
|
|
from views.custom_commands import (
|
|
CustomCommandCreateModal,
|
|
CustomCommandEditModal,
|
|
CustomCommandManagementView,
|
|
CustomCommandListView,
|
|
CustomCommandSearchModal,
|
|
SingleCommandManagementView
|
|
)
|
|
from exceptions import BotException
|
|
|
|
|
|
class CustomCommandsCommands(commands.Cog):
|
|
"""Custom commands slash command handlers."""
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
self.logger = get_contextual_logger(f'{__name__}.CustomCommandsCommands')
|
|
self.logger.info("CustomCommandsCommands cog initialized")
|
|
|
|
@app_commands.command(name="cc", description="Execute a custom command")
|
|
@app_commands.describe(name="Name of the custom command to execute")
|
|
@logged_command("/cc")
|
|
async def execute_custom_command(self, interaction: discord.Interaction, name: str):
|
|
"""Execute a custom command."""
|
|
await interaction.response.defer()
|
|
|
|
try:
|
|
# Execute the command and get response
|
|
command, response_content = await custom_commands_service.execute_command(name)
|
|
|
|
except CustomCommandNotFoundError:
|
|
embed = EmbedTemplate.error(
|
|
title="Command Not Found",
|
|
description=f"No custom command named `{name}` exists.\nUse `/cc-list` to see available commands."
|
|
)
|
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
|
return
|
|
|
|
# # Create embed with the response
|
|
# embed = EmbedTemplate.create_base_embed(
|
|
# title=f"🎮 {command.name}",
|
|
# description=response_content,
|
|
# color=EmbedColors.PRIMARY
|
|
# )
|
|
|
|
# # Add creator info in footer
|
|
# embed.set_footer(
|
|
# text=f"Created by {command.creator.username} • Used {command.use_count} times"
|
|
# )
|
|
|
|
# Send with mentions enabled (users and roles, but not @everyone/@here)
|
|
await interaction.followup.send(
|
|
content=response_content,
|
|
allowed_mentions=discord.AllowedMentions(
|
|
users=True, # Allow user mentions (<@123456789>)
|
|
roles=True, # Allow role mentions (<@&987654321>)
|
|
everyone=False # Block @everyone/@here (already validated)
|
|
)
|
|
)
|
|
|
|
@execute_custom_command.autocomplete('name')
|
|
async def execute_custom_command_autocomplete(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
current: str
|
|
) -> List[app_commands.Choice[str]]:
|
|
"""Provide autocomplete for custom command names."""
|
|
try:
|
|
# Get command names matching the current input
|
|
command_names = await custom_commands_service.get_command_names_for_autocomplete(
|
|
partial_name=current,
|
|
limit=25
|
|
)
|
|
|
|
return [
|
|
app_commands.Choice(name=name, value=name)
|
|
for name in command_names
|
|
]
|
|
except Exception:
|
|
# Return empty list on error
|
|
return []
|
|
|
|
@app_commands.command(name="cc-create", description="Create a new custom command")
|
|
@requires_team()
|
|
@logged_command("/cc-create")
|
|
async def create_custom_command(self, interaction: discord.Interaction):
|
|
"""Create a new custom command using an interactive modal."""
|
|
# Show the creation modal
|
|
modal = CustomCommandCreateModal()
|
|
await interaction.response.send_modal(modal)
|
|
|
|
# Modal's on_submit will show preview with confirmation buttons
|
|
# The CustomCommandCreateConfirmationView handles actual command creation
|
|
|
|
@app_commands.command(name="cc-edit", description="Edit one of your custom commands")
|
|
@app_commands.describe(name="Name of the command to edit")
|
|
@requires_team()
|
|
@logged_command("/cc-edit")
|
|
async def edit_custom_command(self, interaction: discord.Interaction, name: str):
|
|
"""Edit an existing custom command."""
|
|
try:
|
|
# Get the command
|
|
command = await custom_commands_service.get_command_by_name(name)
|
|
|
|
# Check if user owns the command
|
|
if command.creator.discord_id != interaction.user.id: # type: ignore / get_command returns or raises
|
|
embed = EmbedTemplate.error(
|
|
title="Permission Denied",
|
|
description="You can only edit commands that you created."
|
|
)
|
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
return
|
|
|
|
# Show edit modal
|
|
modal = CustomCommandEditModal(command)
|
|
await interaction.response.send_modal(modal)
|
|
|
|
# Wait for modal completion
|
|
await modal.wait()
|
|
|
|
if not modal.is_submitted:
|
|
return
|
|
|
|
# Update the command
|
|
updated_command = await custom_commands_service.update_command(
|
|
name=command.name,
|
|
new_content=modal.result['content'],
|
|
updater_discord_id=interaction.user.id,
|
|
new_tags=modal.result.get('tags')
|
|
)
|
|
|
|
# Success embed
|
|
embed = EmbedTemplate.success(
|
|
title="Command Updated!",
|
|
description=f"Your command `/cc {updated_command.name}` has been updated successfully."
|
|
)
|
|
|
|
# Try to edit the original response
|
|
try:
|
|
await interaction.edit_original_response(embed=embed, view=None)
|
|
except (discord.NotFound, discord.HTTPException):
|
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
|
|
|
except CustomCommandNotFoundError:
|
|
embed = EmbedTemplate.error(
|
|
title="Command Not Found",
|
|
description=f"No custom command named `{name}` exists."
|
|
)
|
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Failed to edit custom command",
|
|
command_name=name,
|
|
user_id=interaction.user.id,
|
|
error=e)
|
|
embed = EmbedTemplate.error(
|
|
title="Edit Failed",
|
|
description="An error occurred while editing your command. Please try again."
|
|
)
|
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
|
|
@edit_custom_command.autocomplete('name')
|
|
async def edit_custom_command_autocomplete(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
current: str
|
|
) -> List[app_commands.Choice[str]]:
|
|
"""Autocomplete for commands owned by the user."""
|
|
try:
|
|
# Get user's commands
|
|
search_result = await custom_commands_service.get_commands_by_creator(
|
|
creator_discord_id=interaction.user.id,
|
|
page=1,
|
|
page_size=25
|
|
)
|
|
|
|
# Filter by current input
|
|
matching_commands = [
|
|
cmd for cmd in search_result.commands
|
|
if current.lower() in cmd.name.lower()
|
|
]
|
|
|
|
return [
|
|
app_commands.Choice(name=cmd.name, value=cmd.name)
|
|
for cmd in matching_commands[:25]
|
|
]
|
|
except Exception:
|
|
return []
|
|
|
|
@app_commands.command(name="cc-delete", description="Delete one of your custom commands")
|
|
@app_commands.describe(name="Name of the command to delete")
|
|
@requires_team()
|
|
@logged_command("/cc-delete")
|
|
async def delete_custom_command(self, interaction: discord.Interaction, name: str):
|
|
"""Delete a custom command with confirmation."""
|
|
try:
|
|
# Get the command
|
|
command = await custom_commands_service.get_command_by_name(name)
|
|
|
|
# Check if user owns the command
|
|
if command.creator.discord_id != interaction.user.id:
|
|
embed = EmbedTemplate.error(
|
|
title="Permission Denied",
|
|
description="You can only delete commands that you created."
|
|
)
|
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
return
|
|
|
|
# Show command management view for deletion
|
|
management_view = SingleCommandManagementView(command, interaction.user.id)
|
|
embed = management_view.create_command_embed()
|
|
|
|
# Override the embed title to emphasize deletion
|
|
embed.title = f"🗑️ Delete Command: {command.name}"
|
|
embed.color = EmbedColors.WARNING
|
|
embed.description = "⚠️ Are you sure you want to delete this command?"
|
|
|
|
await interaction.response.send_message(embed=embed, view=management_view, ephemeral=True)
|
|
|
|
except CustomCommandNotFoundError:
|
|
embed = EmbedTemplate.error(
|
|
title="Command Not Found",
|
|
description=f"No custom command named `{name}` exists."
|
|
)
|
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Failed to show delete interface for custom command",
|
|
command_name=name,
|
|
user_id=interaction.user.id,
|
|
error=e)
|
|
embed = EmbedTemplate.error(
|
|
title="Error",
|
|
description="An error occurred while loading the command. Please try again."
|
|
)
|
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
|
|
|
@delete_custom_command.autocomplete('name')
|
|
async def delete_custom_command_autocomplete(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
current: str
|
|
) -> List[app_commands.Choice[str]]:
|
|
"""Autocomplete for commands owned by the user."""
|
|
# NOTE: Originally was: return await self.edit_custom_command_autocomplete(interaction, current)
|
|
# But Pylance complained about "Expected 1 positional argument" so duplicated logic instead
|
|
try:
|
|
# Get user's commands
|
|
search_result = await custom_commands_service.get_commands_by_creator(
|
|
creator_discord_id=interaction.user.id,
|
|
page=1,
|
|
page_size=25
|
|
)
|
|
|
|
# Filter by current input
|
|
matching_commands = [
|
|
cmd for cmd in search_result.commands
|
|
if current.lower() in cmd.name.lower()
|
|
]
|
|
|
|
return [
|
|
app_commands.Choice(name=cmd.name, value=cmd.name)
|
|
for cmd in matching_commands[:25]
|
|
]
|
|
except Exception:
|
|
return []
|
|
|
|
@app_commands.command(name="cc-mine", description="View and manage your custom commands")
|
|
@requires_team()
|
|
@logged_command("/cc-mine")
|
|
async def my_custom_commands(self, interaction: discord.Interaction):
|
|
"""Show user's custom commands with management interface."""
|
|
await interaction.response.defer(ephemeral=True)
|
|
|
|
try:
|
|
# Get user's commands
|
|
search_result = await custom_commands_service.get_commands_by_creator(
|
|
creator_discord_id=interaction.user.id,
|
|
page=1,
|
|
page_size=100 # Get all commands for management
|
|
)
|
|
|
|
if not search_result.commands:
|
|
embed = EmbedTemplate.info(
|
|
title="Your Custom Commands",
|
|
description="You haven't created any custom commands yet!"
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Get Started",
|
|
value="Use `/cc-create` to create your first custom command.",
|
|
inline=False
|
|
)
|
|
|
|
embed.add_field(
|
|
name="Explore",
|
|
value="Use `/cc-list` to see what commands others have created.",
|
|
inline=False
|
|
)
|
|
|
|
await interaction.followup.send(embed=embed)
|
|
return
|
|
|
|
# Create management view
|
|
management_view = CustomCommandManagementView(
|
|
commands=search_result.commands,
|
|
user_id=interaction.user.id
|
|
)
|
|
|
|
embed = management_view.get_embed()
|
|
await interaction.followup.send(embed=embed, view=management_view)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Failed to load user's custom commands",
|
|
user_id=interaction.user.id,
|
|
error=e)
|
|
embed = EmbedTemplate.error(
|
|
title="Load Failed",
|
|
description="An error occurred while loading your commands. Please try again."
|
|
)
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
@app_commands.command(name="cc-list", description="Browse all custom commands")
|
|
@app_commands.describe(
|
|
creator="Filter by creator username",
|
|
search="Search in command names",
|
|
popular="Show only popular commands (10+ uses)"
|
|
)
|
|
@logged_command("/cc-list")
|
|
async def list_custom_commands(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
creator: Optional[str] = None,
|
|
search: Optional[str] = None,
|
|
popular: bool = False
|
|
):
|
|
"""Browse custom commands with filtering options."""
|
|
await interaction.response.defer()
|
|
|
|
try:
|
|
# Build search filters
|
|
filters = CustomCommandSearchFilters(
|
|
name_contains=search,
|
|
creator_name=creator,
|
|
min_uses=10 if popular else None,
|
|
sort_by='popularity' if popular else 'name',
|
|
sort_desc=popular,
|
|
page=1,
|
|
page_size=50
|
|
)
|
|
|
|
# Search for commands
|
|
search_result = await custom_commands_service.search_commands(filters)
|
|
|
|
# Create list view
|
|
list_view = CustomCommandListView(
|
|
search_result=search_result,
|
|
user_id=interaction.user.id
|
|
)
|
|
|
|
embed = list_view.get_current_embed()
|
|
|
|
# Add search info to embed
|
|
search_info = []
|
|
if creator:
|
|
search_info.append(f"Creator: {creator}")
|
|
if search:
|
|
search_info.append(f"Name contains: {search}")
|
|
if popular:
|
|
search_info.append("Popular commands only")
|
|
|
|
if search_info:
|
|
embed.add_field(
|
|
name="🔍 Filters Applied",
|
|
value=" • ".join(search_info),
|
|
inline=False
|
|
)
|
|
|
|
await interaction.followup.send(embed=embed, view=list_view)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Failed to list custom commands",
|
|
user_id=interaction.user.id,
|
|
error=e)
|
|
embed = EmbedTemplate.error(
|
|
title="Search Failed",
|
|
description="An error occurred while searching for commands. Please try again."
|
|
)
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
@app_commands.command(name="cc-search", description="Advanced search for custom commands")
|
|
@logged_command("/cc-search")
|
|
async def search_custom_commands(self, interaction: discord.Interaction):
|
|
"""Advanced search for custom commands using a modal."""
|
|
# Show search modal
|
|
modal = CustomCommandSearchModal()
|
|
await interaction.response.send_modal(modal)
|
|
|
|
# Wait for modal completion
|
|
await modal.wait()
|
|
|
|
if not modal.is_submitted:
|
|
return
|
|
|
|
try:
|
|
# Build search filters from modal results
|
|
filters = CustomCommandSearchFilters(
|
|
name_contains=modal.result.get('name_contains'),
|
|
creator_name=modal.result.get('creator_name'),
|
|
min_uses=modal.result.get('min_uses'),
|
|
sort_by='popularity',
|
|
sort_desc=True,
|
|
page=1,
|
|
page_size=50
|
|
)
|
|
|
|
# Search for commands
|
|
search_result = await custom_commands_service.search_commands(filters)
|
|
|
|
# Create list view
|
|
list_view = CustomCommandListView(
|
|
search_result=search_result,
|
|
user_id=interaction.user.id
|
|
)
|
|
|
|
embed = list_view.get_current_embed()
|
|
|
|
# Try to edit the original response
|
|
try:
|
|
await interaction.edit_original_response(embed=embed, view=list_view)
|
|
except (discord.NotFound, discord.HTTPException):
|
|
await interaction.followup.send(embed=embed, view=list_view)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Failed to search custom commands",
|
|
user_id=interaction.user.id,
|
|
error=e)
|
|
embed = EmbedTemplate.error(
|
|
title="Search Failed",
|
|
description="An error occurred while searching. Please try again."
|
|
)
|
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
|
|
|
@app_commands.command(name="cc-info", description="Get detailed information about a custom command")
|
|
@app_commands.describe(name="Name of the command to get info about")
|
|
@logged_command("/cc-info")
|
|
async def info_custom_command(self, interaction: discord.Interaction, name: str):
|
|
"""Get detailed information about a custom command."""
|
|
await interaction.response.defer()
|
|
|
|
try:
|
|
# Get the command
|
|
command = await custom_commands_service.get_command_by_name(name)
|
|
|
|
# Create detailed info embed
|
|
embed = EmbedTemplate.create_base_embed(
|
|
title=f"📊 Command Info: {command.name}",
|
|
description="Detailed information about this custom command",
|
|
color=EmbedColors.INFO
|
|
)
|
|
|
|
# Basic info
|
|
embed.add_field(
|
|
name="Response",
|
|
value=command.content[:500] + ('...' if len(command.content) > 500 else ''),
|
|
inline=False
|
|
)
|
|
|
|
# Creator info
|
|
creator_text = f"**Username:** {command.creator.username}\n"
|
|
if command.creator.display_name:
|
|
creator_text += f"**Display Name:** {command.creator.display_name}\n"
|
|
creator_text += f"**Total Commands:** {command.creator.active_commands}"
|
|
|
|
embed.add_field(
|
|
name="👤 Creator",
|
|
value=creator_text,
|
|
inline=True
|
|
)
|
|
|
|
# Usage statistics
|
|
stats_text = f"**Total Uses:** {command.use_count}\n"
|
|
stats_text += f"**Popularity Score:** {command.popularity_score:.1f}/10\n"
|
|
stats_text += f"**Created:** <t:{int(command.created_at.timestamp())}:R>\n"
|
|
|
|
if command.last_used:
|
|
stats_text += f"**Last Used:** <t:{int(command.last_used.timestamp())}:R>\n"
|
|
else:
|
|
stats_text += "**Last Used:** Never\n"
|
|
|
|
if command.updated_at:
|
|
stats_text += f"**Last Updated:** <t:{int(command.updated_at.timestamp())}:R>"
|
|
|
|
embed.add_field(
|
|
name="📈 Statistics",
|
|
value=stats_text,
|
|
inline=True
|
|
)
|
|
|
|
# Tags
|
|
if command.tags:
|
|
embed.add_field(
|
|
name="🏷️ Tags",
|
|
value=', '.join(command.tags),
|
|
inline=False
|
|
)
|
|
|
|
# Usage instructions
|
|
embed.add_field(
|
|
name="💡 How to Use",
|
|
value=f"Type `/cc {command.name}` to execute this command",
|
|
inline=False
|
|
)
|
|
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
except CustomCommandNotFoundError:
|
|
embed = EmbedTemplate.error(
|
|
title="Command Not Found",
|
|
description=f"No custom command named `{name}` exists.\nUse `/cc-list` to see available commands."
|
|
)
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
except Exception as e:
|
|
self.logger.error("Failed to get custom command info",
|
|
command_name=name,
|
|
user_id=interaction.user.id,
|
|
error=e)
|
|
embed = EmbedTemplate.error(
|
|
title="Info Failed",
|
|
description="An error occurred while getting command information."
|
|
)
|
|
await interaction.followup.send(embed=embed)
|
|
|
|
@info_custom_command.autocomplete('name')
|
|
async def info_custom_command_autocomplete(
|
|
self,
|
|
interaction: discord.Interaction,
|
|
current: str
|
|
) -> List[app_commands.Choice[str]]:
|
|
"""Autocomplete for all command names."""
|
|
# NOTE: Originally was: return await self.execute_custom_command_autocomplete(interaction, current)
|
|
# But Pylance complained about "Expected 1 positional argument" so duplicated logic instead
|
|
try:
|
|
# Get command names matching the current input
|
|
command_names = await custom_commands_service.get_command_names_for_autocomplete(
|
|
partial_name=current,
|
|
limit=25
|
|
)
|
|
|
|
return [
|
|
app_commands.Choice(name=name, value=name)
|
|
for name in command_names
|
|
]
|
|
except Exception:
|
|
# Return empty list on error
|
|
return [] |