from typing import Any
import discord
from bot.weakauras_bot import WeakAurasBot
from discord import app_commands
from utils.django_permissions import (
check_server_permission,
get_permission_error_message,
get_server_permission_config,
)
from utils.logging import get_logger, log_command
from views.embed_builder import EmbedBuilderView
logger = get_logger(__name__)
[docs]
class MacroListView(discord.ui.View):
"""A paginated view for displaying macro lists"""
[docs]
def __init__(
self, macros: dict, guild_name: str, bot: WeakAurasBot, items_per_page: int = 50
):
super().__init__(timeout=300) # 5 minute timeout
self.macros = macros
self.guild_name = guild_name
self.bot = bot
self.items_per_page = items_per_page
self.current_page = 0
# Convert macros dict to list of tuples for easier pagination
self.macro_items = list(macros.items())
self.total_pages = (
len(self.macro_items) + items_per_page - 1
) // items_per_page
# Update button states
self._update_buttons()
def _update_buttons(self):
"""Update button states based on current page"""
# Remove all items first
self.clear_items()
# Only add navigation buttons if we have multiple pages
if self.total_pages > 1:
# Previous button
prev_button = discord.ui.Button(
style=discord.ButtonStyle.secondary,
emoji="⬅️",
disabled=self.current_page == 0,
custom_id="prev_page",
)
prev_button.callback = self._prev_page
self.add_item(prev_button)
# Page indicator button (disabled, just for display)
page_button = discord.ui.Button(
style=discord.ButtonStyle.secondary,
label=f"Page {self.current_page + 1}/{self.total_pages}",
disabled=True,
custom_id="page_indicator",
)
self.add_item(page_button)
# Next button
next_button = discord.ui.Button(
style=discord.ButtonStyle.secondary,
emoji="➡️",
disabled=self.current_page >= self.total_pages - 1,
custom_id="next_page",
)
next_button.callback = self._next_page
self.add_item(next_button)
[docs]
def create_embed(self) -> tuple[discord.Embed, discord.File | None]:
"""Create the embed for the current page"""
start_idx = self.current_page * self.items_per_page
end_idx = min(start_idx + self.items_per_page, len(self.macro_items))
page_items = self.macro_items[start_idx:end_idx]
# Build macro list with type indicators for current page
macro_lines = []
for name, data in page_items:
if isinstance(data, dict) and data.get("type") == "embed":
macro_lines.append(f"📄 {name} *(embed)*")
else:
macro_lines.append(f"💬 {name} *(text)*")
macro_list = "\n".join(macro_lines)
# Add page info to title if multiple pages
title = "📂 WeakAuras Macros"
if self.total_pages > 1:
title += f" (Page {self.current_page + 1}/{self.total_pages})"
embed, logo_file = self.bot.create_embed(
title=title,
description=macro_list,
footer_text=f"Server: {self.guild_name} • Total macros: {len(self.macro_items)}",
)
return embed, logo_file
async def _prev_page(self, interaction: discord.Interaction):
"""Handle previous page button click"""
if self.current_page > 0:
self.current_page -= 1
self._update_buttons()
embed, logo_file = self.create_embed()
# Update the message
kwargs = {"embed": embed, "view": self}
if logo_file:
kwargs["attachments"] = [logo_file]
await interaction.response.edit_message(**kwargs)
async def _next_page(self, interaction: discord.Interaction):
"""Handle next page button click"""
if self.current_page < self.total_pages - 1:
self.current_page += 1
self._update_buttons()
embed, logo_file = self.create_embed()
# Update the message
kwargs = {"embed": embed, "view": self}
if logo_file:
kwargs["attachments"] = [logo_file]
await interaction.response.edit_message(**kwargs)
[docs]
async def on_timeout(self):
"""Called when the view times out"""
# Disable all buttons when view times out
for item in self.children:
if isinstance(item, discord.ui.Button):
item.disabled = True
[docs]
async def send_embed_response(
interaction: discord.Interaction,
embed: discord.Embed,
logo_file: discord.File | None,
ephemeral: bool = True,
):
"""Helper function to send embed response with optional file attachment"""
kwargs = {"embed": embed, "ephemeral": ephemeral}
if logo_file:
kwargs["file"] = logo_file
await interaction.response.send_message(**kwargs)
[docs]
def setup_macro_commands(bot: WeakAurasBot): # noqa: PLR0915
"""Setup all macro-related slash commands"""
async def macro_name_autocomplete(
interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
"""Autocomplete function for all macro names"""
if not interaction.guild:
return []
guild_id = interaction.guild.id
guild_name = interaction.guild.name
macros = bot.load_server_macros(guild_id, guild_name)
# Filter macro names based on current input
filtered_macros = [name for name in macros if current.lower() in name.lower()]
# Return up to 25 choices (Discord limit)
return [
app_commands.Choice(name=name, value=name) for name in filtered_macros[:25]
]
async def text_macro_autocomplete(
interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
"""Autocomplete function for text macro names only"""
if not interaction.guild:
return []
guild_id = interaction.guild.id
guild_name = interaction.guild.name
macros = bot.load_server_macros(guild_id, guild_name)
# Filter for text macros only and current input
filtered_macros = []
for name, data in macros.items():
if current.lower() in name.lower():
# Check if it's a text macro (not embed)
if isinstance(data, dict):
macro_type = data.get("type", "text")
if macro_type == "text":
filtered_macros.append(name)
else:
# Legacy format is always text
filtered_macros.append(name)
# Return up to 25 choices (Discord limit)
return [
app_commands.Choice(name=name, value=name) for name in filtered_macros[:25]
]
async def embed_macro_autocomplete(
interaction: discord.Interaction, current: str
) -> list[app_commands.Choice[str]]:
"""Autocomplete function for embed macro names only"""
if not interaction.guild:
return []
guild_id = interaction.guild.id
guild_name = interaction.guild.name
macros = bot.load_server_macros(guild_id, guild_name)
# Filter for embed macros only and current input
filtered_macros = []
for name, data in macros.items():
if current.lower() in name.lower() and isinstance(data, dict):
macro_type = data.get("type", "text")
if macro_type == "embed":
filtered_macros.append(name)
# Note: Legacy format is never embed, so we skip those
# Return up to 25 choices (Discord limit)
return [
app_commands.Choice(name=name, value=name) for name in filtered_macros[:25]
]
@bot.tree.command(
name="create_macro", description="Create a new WeakAuras macro command"
)
@log_command
async def create_macro(interaction: discord.Interaction, name: str, message: str):
"""Create a new macro with the given name and message"""
if not interaction.guild:
logger.warning("create_macro command used outside of server")
await interaction.response.send_message(
"This command can only be used in a server!", ephemeral=True
)
return
guild_id = interaction.guild.id
guild_name = interaction.guild.name
# Check if user has permission to create macros
if not isinstance(
interaction.user, discord.Member
) or not check_server_permission(interaction.user, guild_id, "create_macros"):
config = get_server_permission_config(guild_id)
error_message = get_permission_error_message("create_macros", config)
embed, logo_file = bot.create_embed(
title="❌ Permission Denied",
description=error_message,
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
logger.warning("create_macro command denied - insufficient permissions")
return
# Load server-specific macros
macros = bot.load_server_macros(guild_id, guild_name)
if name in macros:
logger.info(f"create_macro failed - macro '{name}' already exists")
await interaction.response.send_message(
f"WeakAuras macro '{name}' already exists!", ephemeral=True
)
return
# Store as JSON-formatted macro data
macro_data = {
"name": name,
"message": message,
"created_by": str(interaction.user.id),
"created_by_name": interaction.user.name,
"created_at": interaction.created_at.isoformat(),
}
macros[name] = macro_data
bot.save_server_macros(guild_id, guild_name, macros)
logger.info(
f"Successfully created macro '{name}' in guild {guild_name} ({guild_id})"
)
# Create branded success embed
embed, logo_file = bot.create_embed(
title="✅ Macro Created",
description=f"Successfully created macro **{name}**",
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
@bot.tree.command(
name="create_embed_macro",
description="Create a new WeakAuras embed macro with rich formatting",
)
@log_command
async def create_embed_macro(interaction: discord.Interaction, name: str):
"""Create a new embed macro with rich formatting"""
if not interaction.guild:
logger.warning("create_embed_macro command used outside of server")
await interaction.response.send_message(
"This command can only be used in a server!", ephemeral=True
)
return
guild_id = interaction.guild.id
guild_name = interaction.guild.name
# Check if user has permission to create macros
if not isinstance(
interaction.user, discord.Member
) or not check_server_permission(interaction.user, guild_id, "create_macros"):
config = get_server_permission_config(guild_id)
error_message = get_permission_error_message("create_macros", config)
embed, logo_file = bot.create_embed(
title="❌ Permission Denied",
description=error_message,
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
logger.warning(
"create_embed_macro command denied - insufficient permissions"
)
return
# Load server-specific macros
macros = bot.load_server_macros(guild_id, guild_name)
if name in macros:
logger.info(f"create_embed_macro failed - macro '{name}' already exists")
await interaction.response.send_message(
f"WeakAuras macro '{name}' already exists!", ephemeral=True
)
return
async def save_embed_macro(
save_interaction: discord.Interaction, embed_data: dict[str, Any]
):
"""Callback to save the embed macro"""
# Store as JSON-formatted macro data with embed type
macro_data = {
"name": name,
"type": "embed",
"embed_data": embed_data,
"created_by": str(interaction.user.id),
"created_by_name": interaction.user.name,
"created_at": interaction.created_at.isoformat(),
}
macros[name] = macro_data
bot.save_server_macros(guild_id, guild_name, macros)
logger.info(
f"Successfully created embed macro '{name}' in guild {guild_name} ({guild_id})"
)
# Create branded success embed
success_embed, logo_file = bot.create_embed(
title="✅ Embed Macro Created",
description=f"Successfully created embed macro **{name}**",
footer_text=f"Server: {guild_name}",
)
if logo_file:
await save_interaction.response.send_message(
embed=success_embed, file=logo_file, ephemeral=True
)
else:
await save_interaction.response.send_message(
embed=success_embed, ephemeral=True
)
# Create embed builder view
embed_view = EmbedBuilderView(macro_name=name, callback_func=save_embed_macro)
# Create initial preview
preview_embed = embed_view.create_preview_embed()
content = f"**Building Embed Macro: `{name}`**\n*Status: No content added yet*"
await interaction.response.send_message(
content=content, embed=preview_embed, view=embed_view, ephemeral=True
)
@bot.tree.command(
name="list_macros", description="List all available WeakAuras macros"
)
@log_command
async def list_macros(interaction: discord.Interaction):
"""List all available macros for this server"""
if not interaction.guild:
logger.warning("list_macros command used outside of server")
await interaction.response.send_message(
"This command can only be used in a server!", ephemeral=True
)
return
guild_id = interaction.guild.id
guild_name = interaction.guild.name
macros = bot.load_server_macros(guild_id, guild_name)
if not macros:
logger.info(
f"list_macros returned 0 macros for guild {guild_name} ({guild_id})"
)
embed, logo_file = bot.create_embed(
title="📂 No Macros Found",
description="No WeakAuras macros available in this server.",
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
return
logger.info(
f"list_macros returned {len(macros)} macros for guild {guild_name} ({guild_id}): {', '.join(macros.keys())}"
)
# Create paginated view for macro list
macro_view = MacroListView(macros, guild_name, bot, items_per_page=50)
embed, logo_file = macro_view.create_embed()
# Send with view (pagination buttons) if multiple pages, otherwise without view
if macro_view.total_pages > 1:
# Use view for pagination
if logo_file:
await interaction.response.send_message(
embed=embed, file=logo_file, view=macro_view, ephemeral=True
)
else:
await interaction.response.send_message(
embed=embed, view=macro_view, ephemeral=True
)
# No pagination needed
elif logo_file:
await interaction.response.send_message(
embed=embed, file=logo_file, ephemeral=True
)
else:
await interaction.response.send_message(embed=embed, ephemeral=True)
@bot.tree.command(
name="delete_macro",
description="Delete an existing WeakAuras macro (Admin only)",
)
@app_commands.autocomplete(name=macro_name_autocomplete)
@log_command
async def delete_macro(interaction: discord.Interaction, name: str):
"""Delete a macro from this server (requires admin role)"""
if not interaction.guild:
logger.warning("delete_macro command used outside of server")
await interaction.response.send_message(
"This command can only be used in a server!", ephemeral=True
)
return
# Check if user has permission to delete macros
if not isinstance(
interaction.user, discord.Member
) or not check_server_permission(
interaction.user, interaction.guild.id, "delete_macros"
):
config = get_server_permission_config(interaction.guild.id)
error_message = get_permission_error_message("delete_macros", config)
embed, logo_file = bot.create_embed(
title="❌ Permission Denied",
description=error_message,
footer_text=f"Server: {interaction.guild.name}",
)
await send_embed_response(interaction, embed, logo_file)
logger.warning("delete_macro command denied - insufficient permissions")
return
guild_id = interaction.guild.id
guild_name = interaction.guild.name
macros = bot.load_server_macros(guild_id, guild_name)
if name not in macros:
logger.info(
f"delete_macro failed - macro '{name}' does not exist in guild {guild_name} ({guild_id})"
)
embed, logo_file = bot.create_embed(
title="❌ Macro Not Found",
description=f"WeakAuras macro '{name}' does not exist!",
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
return
del macros[name]
bot.save_server_macros(guild_id, guild_name, macros)
logger.info(
f"Successfully deleted macro '{name}' from guild {guild_name} ({guild_id})"
)
embed, logo_file = bot.create_embed(
title="🗑️ Macro Deleted",
description=f"Successfully deleted macro **{name}**",
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
@bot.tree.command(
name="edit_macro",
description="Edit an existing WeakAuras macro (text or embed)",
)
@app_commands.autocomplete(name=macro_name_autocomplete)
@log_command
async def edit_macro(interaction: discord.Interaction, name: str):
"""Edit an existing macro (automatically detects text vs embed type)"""
if not interaction.guild:
logger.warning("edit_macro command used outside of server")
await interaction.response.send_message(
"This command can only be used in a server!", ephemeral=True
)
return
guild_id = interaction.guild.id
guild_name = interaction.guild.name
# Check if user has permission to edit macros
if not isinstance(
interaction.user, discord.Member
) or not check_server_permission(interaction.user, guild_id, "edit_macros"):
config = get_server_permission_config(guild_id)
error_message = get_permission_error_message("edit_macros", config)
embed, logo_file = bot.create_embed(
title="❌ Permission Denied",
description=error_message,
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
logger.warning("edit_macro command denied - insufficient permissions")
return
# Load server-specific macros
macros = bot.load_server_macros(guild_id, guild_name)
if name not in macros:
logger.info(f"edit_macro failed - macro '{name}' does not exist")
await interaction.response.send_message(
f"WeakAuras macro '{name}' does not exist!", ephemeral=True
)
return
macro_data = macros[name]
# Check if this is an embed macro
if isinstance(macro_data, dict) and macro_data.get("type") == "embed":
# Redirect to embed editing
async def update_embed_macro(
save_interaction: discord.Interaction, embed_data: dict[str, Any]
):
"""Callback to update the embed macro"""
macro_data["embed_data"] = embed_data
macro_data["modified_by"] = str(interaction.user.id)
macro_data["modified_by_name"] = interaction.user.name
macro_data["modified_at"] = interaction.created_at.isoformat()
macros[name] = macro_data
bot.save_server_macros(guild_id, guild_name, macros)
logger.info(
f"Successfully updated embed macro '{name}' via edit_macro in guild {guild_name} ({guild_id})"
)
success_embed, logo_file = bot.create_embed(
title="✅ Embed Macro Updated",
description=f"Successfully updated embed macro **{name}**",
footer_text=f"Server: {guild_name}",
)
if logo_file:
await save_interaction.response.send_message(
embed=success_embed, file=logo_file, ephemeral=True
)
else:
await save_interaction.response.send_message(
embed=success_embed, ephemeral=True
)
# Create embed builder view with existing data
existing_embed_data = macro_data.get("embed_data", {})
embed_view = EmbedBuilderView(
macro_name=name,
embed_data=existing_embed_data,
callback_func=update_embed_macro,
)
preview_embed = embed_view.create_preview_embed()
content = f"**Editing Embed Macro: `{name}`**\n*Use the buttons below to modify your embed.*"
await interaction.response.send_message(
content=content, embed=preview_embed, view=embed_view, ephemeral=True
)
else:
# Handle text macro editing with a simple modal
class TextMacroEditModal(discord.ui.Modal):
def __init__(self, current_message: str):
super().__init__(title=f"Edit Text Macro: {name}")
self.message_input = discord.ui.TextInput(
label="Macro Message",
placeholder="Enter the macro content...",
default=current_message,
style=discord.TextStyle.paragraph,
max_length=2000,
required=True,
)
self.add_item(self.message_input)
async def on_submit(self, modal_interaction: discord.Interaction):
new_message = self.message_input.value.strip()
if not new_message:
await modal_interaction.response.send_message(
"❌ Macro message cannot be empty!", ephemeral=True
)
return
# Update the macro
if isinstance(macro_data, dict):
macro_data["message"] = new_message
macro_data["updated_by"] = str(interaction.user.id)
macro_data["updated_by_name"] = interaction.user.name
macro_data["updated_at"] = interaction.created_at.isoformat()
macros[name] = macro_data
else:
# Convert legacy format to modern format with update info
macros[name] = {
"name": name,
"message": new_message,
"created_by": "",
"created_by_name": "Unknown",
"created_at": "",
"updated_by": str(interaction.user.id),
"updated_by_name": interaction.user.name,
"updated_at": interaction.created_at.isoformat(),
}
bot.save_server_macros(guild_id, guild_name, macros)
logger.info(
f"Successfully updated text macro '{name}' via edit_macro in guild {guild_name} ({guild_id})"
)
success_embed, logo_file = bot.create_embed(
title="✅ Text Macro Updated",
description=f"Successfully updated text macro **{name}**",
footer_text=f"Server: {guild_name}",
)
if logo_file:
await modal_interaction.response.send_message(
embed=success_embed, file=logo_file, ephemeral=True
)
else:
await modal_interaction.response.send_message(
embed=success_embed, ephemeral=True
)
# Get current message
current_message = (
macro_data.get("message", macro_data)
if isinstance(macro_data, dict)
else macro_data
)
modal = TextMacroEditModal(current_message)
await interaction.response.send_modal(modal)
@bot.tree.command(
name="edit_embed_macro",
description="Edit an existing WeakAuras embed macro",
)
@app_commands.autocomplete(name=embed_macro_autocomplete)
@log_command
async def edit_embed_macro(interaction: discord.Interaction, name: str):
"""Edit an existing embed macro"""
if not interaction.guild:
logger.warning("edit_embed_macro command used outside of server")
await interaction.response.send_message(
"This command can only be used in a server!", ephemeral=True
)
return
guild_id = interaction.guild.id
guild_name = interaction.guild.name
# Check if user has permission to create/edit macros
if not isinstance(
interaction.user, discord.Member
) or not check_server_permission(interaction.user, guild_id, "create_macros"):
config = get_server_permission_config(guild_id)
error_message = get_permission_error_message("create_macros", config)
embed, logo_file = bot.create_embed(
title="❌ Permission Denied",
description=error_message,
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
logger.warning("edit_embed_macro command denied - insufficient permissions")
return
# Load server-specific macros
macros = bot.load_server_macros(guild_id, guild_name)
if name not in macros:
logger.info(f"edit_embed_macro failed - macro '{name}' does not exist")
await interaction.response.send_message(
f"WeakAuras macro '{name}' does not exist!", ephemeral=True
)
return
macro_data = macros[name]
# Check if this is an embed macro
if not (isinstance(macro_data, dict) and macro_data.get("type") == "embed"):
await interaction.response.send_message(
f"❌ Macro '{name}' is not an embed macro! Use `/create_embed_macro` to create a new embed version.",
ephemeral=True,
)
return
async def update_embed_macro(
save_interaction: discord.Interaction, embed_data: dict[str, Any]
):
"""Callback to update the embed macro"""
# Update the existing macro data
macro_data["embed_data"] = embed_data
macro_data["modified_by"] = str(interaction.user.id)
macro_data["modified_by_name"] = interaction.user.name
macro_data["modified_at"] = interaction.created_at.isoformat()
macros[name] = macro_data
bot.save_server_macros(guild_id, guild_name, macros)
logger.info(
f"Successfully updated embed macro '{name}' in guild {guild_name} ({guild_id})"
)
# Create branded success embed
success_embed, logo_file = bot.create_embed(
title="✅ Embed Macro Updated",
description=f"Successfully updated embed macro **{name}**",
footer_text=f"Server: {guild_name}",
)
if logo_file:
await save_interaction.response.send_message(
embed=success_embed, file=logo_file, ephemeral=True
)
else:
await save_interaction.response.send_message(
embed=success_embed, ephemeral=True
)
# Create embed builder view with existing data
existing_embed_data = macro_data.get("embed_data", {})
embed_view = EmbedBuilderView(
macro_name=name,
embed_data=existing_embed_data,
callback_func=update_embed_macro,
)
# Create initial preview
preview_embed = embed_view.create_preview_embed()
content = f"**Editing Embed Macro: `{name}`**\n*Use the buttons below to modify your embed.*"
await interaction.response.send_message(
content=content, embed=preview_embed, view=embed_view, ephemeral=True
)
@bot.tree.command(name="macro", description="Execute a saved WeakAuras macro")
@app_commands.autocomplete(name=macro_name_autocomplete)
@log_command
async def execute_macro(interaction: discord.Interaction, name: str):
"""Execute a saved macro from this server"""
if not interaction.guild:
logger.warning("macro command used outside of server")
await interaction.response.send_message(
"This command can only be used in a server!", ephemeral=True
)
return
guild_id = interaction.guild.id
guild_name = interaction.guild.name
macros = bot.load_server_macros(guild_id, guild_name)
if name not in macros:
logger.info(
f"macro execution failed - macro '{name}' does not exist in guild {guild_name} ({guild_id})"
)
embed, logo_file = bot.create_embed(
title="❌ Macro Not Found",
description=f"WeakAuras macro '{name}' does not exist!",
footer_text=f"Server: {guild_name}",
)
await send_embed_response(interaction, embed, logo_file)
return
macro_data = macros[name]
# Check if this is an embed macro
if isinstance(macro_data, dict) and macro_data.get("type") == "embed":
# Handle embed macro
embed_data = macro_data.get("embed_data", {})
embed = discord.Embed()
# Set embed properties
if embed_data.get("title"):
embed.title = embed_data["title"]
if embed_data.get("description"):
embed.description = embed_data["description"]
if embed_data.get("color"):
embed.color = embed_data["color"]
if embed_data.get("footer"):
embed.set_footer(text=embed_data["footer"])
if embed_data.get("image"):
embed.set_image(url=embed_data["image"])
# Add custom fields
for field in embed_data.get("fields", []):
embed.add_field(
name=field["name"],
value=field["value"],
inline=field.get("inline", False),
)
logger.info(
f"Successfully executed embed macro '{name}' from guild {guild_name} ({guild_id})"
)
await interaction.response.send_message(embed=embed)
else:
# Handle regular text macro (backward compatibility)
message = (
macro_data.get("message", macro_data)
if isinstance(macro_data, dict)
else macro_data
)
logger.info(
f"Successfully executed text macro '{name}' from guild {guild_name} ({guild_id})"
)
await interaction.response.send_message(message)