discordbot/cogs/role_selector_cog.py
2025-06-05 21:31:06 -06:00

1853 lines
77 KiB
Python

import discord
from discord.ext import commands
from discord import app_commands # Added for slash commands
from discord.ui import View, Select, select, Modal, TextInput
import json
import os
from typing import List, Dict, Optional, Set, Tuple, Union
import asyncio # Added for sleep
import re # For hex/rgb validation
import uuid # For generating IDs
# Database and Pydantic Models
from api_service.api_server import db
from api_service.api_models import (
RoleOption,
RoleCategoryPreset,
GuildRole,
GuildRoleCategoryConfig,
UserCustomColorRole,
)
async def is_owner_check(interaction: discord.Interaction) -> bool:
"""Checks if the interacting user is the bot owner."""
return interaction.user.id == interaction.client.owner_id
# For color name validation
try:
from matplotlib.colors import is_color_like, to_rgb, XKCD_COLORS
except ImportError:
XKCD_COLORS = {} # Fallback if matplotlib is not installed
def is_color_like(c): # Basic fallback
if isinstance(c, str):
return c.startswith("#") and len(c) in [4, 7]
return False
def to_rgb(c): # Basic fallback
if isinstance(c, str) and c.startswith("#"):
hex_color = c.lstrip("#")
if len(hex_color) == 3:
return tuple(int(hex_color[i] * 2, 16) / 255.0 for i in range(3))
if len(hex_color) == 6:
return tuple(
int(hex_color[i : i + 2], 16) / 255.0 for i in range(0, 6, 2)
)
return (0, 0, 0) # Default black
# --- Constants ---
DEFAULT_ROLE_COLOR = discord.Color.default() # Used for custom color roles initially
# --- Color Parsing Helper ---
def _parse_color_input(color_input: str) -> Optional[discord.Color]:
"""Parses a color input string (hex, rgb, or name) into a discord.Color object."""
color_input = color_input.strip()
# Try hex
hex_match = re.fullmatch(r"#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})", color_input)
if hex_match:
hex_val = hex_match.group(1)
if len(hex_val) == 3:
hex_val = "".join([c * 2 for c in hex_val])
try:
return discord.Color(int(hex_val, 16))
except ValueError:
pass # Should not happen with regex match
# Try RGB: "r, g, b" or "(r, g, b)"
rgb_match = re.fullmatch(
r"\(?\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)?", color_input
)
if rgb_match:
try:
r, g, b = [int(x) for x in rgb_match.groups()]
if all(0 <= val <= 255 for val in (r, g, b)):
return discord.Color.from_rgb(r, g, b)
except ValueError:
pass
# Try English color name (matplotlib XKCD_COLORS)
if XKCD_COLORS: # Check if matplotlib was imported
normalized_input = color_input.lower().replace(" ", "")
# Check against normalized keys
for xkcd_name_key, xkcd_hex_val in XKCD_COLORS.items():
if xkcd_name_key.lower().replace(" ", "") == normalized_input:
try:
# to_rgb for xkcd colors usually returns (r,g,b) float tuple
rgb_float = to_rgb(xkcd_hex_val)
return discord.Color.from_rgb(
int(rgb_float[0] * 255),
int(rgb_float[1] * 255),
int(rgb_float[2] * 255),
)
except Exception:
pass # Matplotlib color conversion failed
break
# Fallback for very common color names if matplotlib is not available
elif color_input.lower() in {
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
"yellow": (255, 255, 0),
"purple": (128, 0, 128),
"orange": (255, 165, 0),
"pink": (255, 192, 203),
"black": (0, 0, 0),
"white": (255, 255, 255),
}:
r, g, b = {
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
"yellow": (255, 255, 0),
"purple": (128, 0, 128),
"orange": (255, 165, 0),
"pink": (255, 192, 203),
"black": (0, 0, 0),
"white": (255, 255, 255),
}[color_input.lower()]
return discord.Color.from_rgb(r, g, b)
return None
# --- Custom Color Modal ---
class CustomColorModal(Modal, title="Set Your Custom Role Color"):
color_input = TextInput(
label="Color (Hex, RGB, or Name)",
placeholder="#RRGGBB, 255,0,128, or 'sky blue'",
style=discord.TextStyle.short,
required=True,
max_length=100,
)
async def on_submit(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True, thinking=True)
guild = interaction.guild
member = interaction.user
if not guild or not isinstance(member, discord.Member):
await interaction.followup.send(
"This can only be used in a server.", ephemeral=True
)
return
parsed_color = _parse_color_input(self.color_input.value)
if not parsed_color:
await interaction.followup.send(
f"Could not understand the color '{self.color_input.value}'.\n"
"Please use a hex code (e.g., `#FF0000`), RGB values (e.g., `255,0,0`), "
"or a known color name (e.g., 'red', 'sky blue').",
ephemeral=True,
)
return
custom_role_name = f"User Color - {member.id}"
existing_user_color_role_db = db.get_user_custom_color_role(
str(guild.id), str(member.id)
)
role_to_update: Optional[discord.Role] = None
if existing_user_color_role_db:
role_to_update = guild.get_role(int(existing_user_color_role_db.role_id))
if not role_to_update:
db.delete_user_custom_color_role(str(guild.id), str(member.id))
existing_user_color_role_db = None
elif (
role_to_update.name != custom_role_name
): # Name mismatch, could be manually changed
try: # Try to rename it back
await role_to_update.edit(
name=custom_role_name,
reason="Standardizing custom color role name",
)
except discord.Forbidden:
await interaction.followup.send(
"I couldn't standardize your existing color role name. Please check my permissions.",
ephemeral=True,
)
# Potentially fall through to create a new one if renaming fails and old one is problematic
except discord.HTTPException:
pass # Non-critical error, proceed with color update
if not role_to_update:
for r in guild.roles: # Check if a role with the target name already exists
if r.name == custom_role_name:
role_to_update = r
break
if not role_to_update:
try:
role_to_update = await guild.create_role(
name=custom_role_name,
color=parsed_color, # Set initial color
reason=f"Custom color role for {member.display_name}",
)
except discord.Forbidden:
await interaction.followup.send(
"I don't have permission to create roles.", ephemeral=True
)
return
except discord.HTTPException as e:
await interaction.followup.send(
f"Failed to create role: {e}", ephemeral=True
)
return
if not role_to_update:
await interaction.followup.send(
"Failed to obtain a role to update.", ephemeral=True
)
return
if role_to_update.color != parsed_color:
try:
await role_to_update.edit(
color=parsed_color, reason=f"Color update for {member.display_name}"
)
except discord.Forbidden:
await interaction.followup.send(
"I don't have permission to edit the role color.", ephemeral=True
)
return
except discord.HTTPException as e:
await interaction.followup.send(
f"Failed to update role color: {e}", ephemeral=True
)
return
roles_to_add_to_member = []
if role_to_update.id not in [r.id for r in member.roles]:
roles_to_add_to_member.append(role_to_update)
roles_to_remove_from_member = [
r
for r in member.roles
if r.name.startswith("User Color - ") and r.id != role_to_update.id
]
try:
if roles_to_remove_from_member:
await member.remove_roles(
*roles_to_remove_from_member,
reason="Cleaning up old custom color roles",
)
if roles_to_add_to_member:
await member.add_roles(
*roles_to_add_to_member, reason="Applied custom color role"
)
except discord.Forbidden:
await interaction.followup.send(
"I don't have permission to assign roles.", ephemeral=True
)
return
except discord.HTTPException as e:
await interaction.followup.send(
f"Failed to assign role: {e}", ephemeral=True
)
return
user_color_role_data = UserCustomColorRole(
user_id=str(member.id),
guild_id=str(guild.id),
role_id=str(role_to_update.id),
hex_color=f"#{parsed_color.value:06x}",
)
db.save_user_custom_color_role(user_color_role_data)
# New logic: Remove roles from "Colors" preset if custom color is set
removed_preset_color_roles_names = []
# Ensure guild is not None, though it should be from earlier checks (line 103)
if guild and isinstance(member, discord.Member): # member check for safety
guild_role_categories = db.get_guild_role_category_configs(str(guild.id))
colors_preset_role_ids_to_remove = set()
for cat_config in guild_role_categories:
# "Colors" preset ID is 'default_colors'
if cat_config.is_preset and cat_config.preset_id == "default_colors":
for role_option in cat_config.roles:
colors_preset_role_ids_to_remove.add(int(role_option.role_id))
break # Found the Colors preset for this guild
if colors_preset_role_ids_to_remove:
roles_to_actually_remove_from_member = []
for member_role in member.roles:
if member_role.id in colors_preset_role_ids_to_remove:
roles_to_actually_remove_from_member.append(member_role)
if roles_to_actually_remove_from_member:
try:
await member.remove_roles(
*roles_to_actually_remove_from_member,
reason="User set a custom color, removing preset color role(s).",
)
removed_preset_color_roles_names = [
r.name for r in roles_to_actually_remove_from_member
]
except discord.Forbidden:
await interaction.followup.send(
"I tried to remove your preset color role(s) but lack permissions.",
ephemeral=True,
)
except discord.HTTPException as e:
await interaction.followup.send(
f"Failed to remove your preset color role(s): {e}",
ephemeral=True,
)
feedback_message = (
f"Your custom role color has been set to {user_color_role_data.hex_color}!"
)
if removed_preset_color_roles_names:
feedback_message += f"\nRemoved preset color role(s): {', '.join(removed_preset_color_roles_names)}."
await interaction.followup.send(feedback_message, ephemeral=True)
async def on_error(self, interaction: discord.Interaction, error: Exception):
await interaction.followup.send(f"An error occurred: {error}", ephemeral=True)
print(f"Error in CustomColorModal: {error}")
# --- View for the Custom Color Button ---
class CustomColorButtonView(View):
def __init__(self):
super().__init__(timeout=None)
@discord.ui.button(
label="Set Custom Role Color",
style=discord.ButtonStyle.primary,
custom_id="persistent_set_custom_color_button",
)
async def set_color_button_callback(
self, interaction: discord.Interaction, button: discord.ui.Button
):
modal = CustomColorModal()
await interaction.response.send_modal(modal)
# --- Persistent View Definition ---
class RoleSelectorView(View):
def __init__(
self, guild_id: int, category_config: GuildRoleCategoryConfig, bot_instance
):
super().__init__(timeout=None)
self.guild_id = guild_id
self.category_config = category_config
self.bot = bot_instance
self.category_role_ids: Set[int] = {
int(role.role_id) for role in category_config.roles
}
self.custom_id = (
f"persistent_role_select_view_{guild_id}_{category_config.category_id}"
)
self.select_chunk_map: Dict[str, Set[int]] = {}
category_display_roles: List[GuildRole] = category_config.roles
self.role_chunks = [
category_display_roles[i : i + 25]
for i in range(0, len(category_display_roles), 25)
]
num_chunks = len(self.role_chunks)
total_max_values = min(
category_config.max_selectable, len(category_display_roles)
)
actual_min_values = 0
for i, chunk in enumerate(self.role_chunks):
options = [
discord.SelectOption(
label=role.name,
value=str(role.role_id),
emoji=role.emoji if role.emoji else None,
)
for role in chunk
]
chunk_role_ids = {int(role.role_id) for role in chunk}
if not options:
continue
chunk_max_values = min(total_max_values, len(options))
placeholder = f"Select {category_config.name} role(s)..."
if num_chunks > 1:
placeholder = (
f"Select {category_config.name} role(s) ({i+1}/{num_chunks})..."
)
select_custom_id = (
f"role_select_dropdown_{guild_id}_{category_config.category_id}_{i}"
)
self.select_chunk_map[select_custom_id] = chunk_role_ids
select_component = Select(
placeholder=placeholder,
min_values=actual_min_values,
max_values=chunk_max_values,
options=options,
custom_id=select_custom_id,
)
select_component.callback = self.select_callback
self.add_item(select_component)
async def select_callback(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True, thinking=True)
member = interaction.user
guild = interaction.guild
if not isinstance(member, discord.Member) or not guild:
await interaction.followup.send(
"This interaction must be used within a server.", ephemeral=True
)
return
if guild.id != self.guild_id:
await interaction.followup.send(
"This role selector is not for this server.", ephemeral=True
)
return
interacted_custom_id = interaction.data["custom_id"]
interacted_chunk_role_ids: Set[int] = self.select_chunk_map.get(
interacted_custom_id, set()
)
if not interacted_chunk_role_ids:
for component in self.children:
if (
isinstance(component, Select)
and component.custom_id == interacted_custom_id
):
interacted_chunk_role_ids = {
int(opt.value) for opt in component.options
}
break
if not interacted_chunk_role_ids:
await interaction.followup.send(
"An internal error occurred identifying roles for this dropdown.",
ephemeral=True,
)
return
selected_values = interaction.data.get("values", [])
selected_role_ids_from_interaction = {int(value) for value in selected_values}
member_category_role_ids = {
role.id for role in member.roles if role.id in self.category_role_ids
}
roles_to_add_ids = selected_role_ids_from_interaction - member_category_role_ids
member_roles_in_interacted_chunk = member_category_role_ids.intersection(
interacted_chunk_role_ids
)
roles_to_remove_ids = (
member_roles_in_interacted_chunk - selected_role_ids_from_interaction
)
if self.category_config.max_selectable == 1 and roles_to_add_ids:
if len(roles_to_add_ids) > 1:
await interaction.followup.send(
f"Error: Cannot select multiple roles for '{self.category_config.name}'.",
ephemeral=True,
)
return
role_to_add_id = list(roles_to_add_ids)[0]
other_member_roles_in_category = member_category_role_ids - {role_to_add_id}
roles_to_remove_ids.update(other_member_roles_in_category)
roles_to_add_ids = {role_to_add_id}
roles_to_add = {
guild.get_role(role_id)
for role_id in roles_to_add_ids
if guild.get_role(role_id)
}
roles_to_remove = {
guild.get_role(role_id)
for role_id in roles_to_remove_ids
if guild.get_role(role_id)
}
added_names, removed_names, error_messages = [], [], []
removed_custom_color_feedback = "" # Initialize here
# New logic: If adding a "Colors" preset role, remove custom color role
# The preset_id for "Colors" is 'default_colors'
is_colors_preset_category = (
self.category_config.is_preset
and self.category_config.preset_id == "default_colors"
)
# A color from the "Colors" preset is being added (roles_to_add is not empty)
if is_colors_preset_category and roles_to_add:
# Ensure member and guild are valid (they should be from earlier checks in lines 262-267)
if isinstance(member, discord.Member) and guild:
existing_user_custom_color_db = db.get_user_custom_color_role(
str(guild.id), str(member.id)
)
if existing_user_custom_color_db:
custom_color_role_to_remove = guild.get_role(
int(existing_user_custom_color_db.role_id)
)
if custom_color_role_to_remove:
try:
await member.remove_roles(
custom_color_role_to_remove,
reason="User selected a preset color, removing custom color role.",
)
db.delete_user_custom_color_role(
str(guild.id), str(member.id)
) # Delete from DB
removed_custom_color_feedback = f"\n- Removed custom color role '{custom_color_role_to_remove.name}'."
except discord.Forbidden:
error_messages.append(
"Could not remove your custom color role (permissions)."
)
except discord.HTTPException as e:
error_messages.append(
f"Error removing custom color role: {e}"
)
else: # Role not found in guild, but was in DB. Clean up DB.
db.delete_user_custom_color_role(str(guild.id), str(member.id))
removed_custom_color_feedback = "\n- Your previous custom color role was not found in the server and has been cleared from my records."
try:
if roles_to_remove:
await member.remove_roles(
*roles_to_remove,
reason=f"Deselected/changed via {self.category_config.name} role selector ({interacted_custom_id})",
)
removed_names = [r.name for r in roles_to_remove if r]
if roles_to_add:
await member.add_roles(
*roles_to_add,
reason=f"Selected via {self.category_config.name} role selector ({interacted_custom_id})",
)
added_names = [r.name for r in roles_to_add if r]
feedback = "Your roles have been updated!"
if added_names:
feedback += f"\n+ Added: {', '.join(added_names)}"
if removed_names:
feedback += f"\n- Removed: {', '.join(removed_names)}"
feedback += removed_custom_color_feedback # Add the custom color removal feedback here
# Adjusted condition for "no changes" message
# Ensure removed_custom_color_feedback is considered. If it has content, changes were made.
if (
not added_names
and not removed_names
and not removed_custom_color_feedback.strip()
):
if selected_values:
feedback = (
"No changes needed for the roles selected in this dropdown."
)
else:
feedback = (
"No roles selected in this dropdown."
if not member_roles_in_interacted_chunk
else "Roles deselected from this dropdown."
)
await interaction.followup.send(feedback, ephemeral=True)
except discord.Forbidden:
error_messages.append("I don't have permission to manage roles.")
except discord.HTTPException as e:
error_messages.append(f"An error occurred: {e}")
except Exception as e:
error_messages.append(f"Unexpected error: {e}")
print(f"Error in role selector: {e}")
if error_messages:
await interaction.followup.send("\n".join(error_messages), ephemeral=True)
class RoleSelectorCog(commands.Cog):
roleselect_group = app_commands.Group(
name="roleselect", description="Manage role selection categories and selectors."
)
rolepreset_group = app_commands.Group(
name="rolepreset", description="Manage global role category presets."
)
def __init__(self, bot):
self.bot = bot
self.bot.loop.create_task(self.register_all_persistent_views())
async def register_all_persistent_views(self):
await self.bot.wait_until_ready()
print("RoleSelectorCog: Registering persistent views...")
registered_count = 0
# Register RoleSelectorView for each guild config
guild_configs_data = db.get_all_guild_role_category_configs()
for guild_id_str, category_configs_list in guild_configs_data.items():
guild_id = int(guild_id_str)
guild = self.bot.get_guild(guild_id)
if not guild:
print(f" Skipping guild {guild_id} (not found).")
continue
for category_config in category_configs_list:
if category_config.roles: # Only register if there are roles
try:
view = RoleSelectorView(guild_id, category_config, self.bot)
self.bot.add_view(view)
registered_count += 1
except Exception as e:
print(
f" Error registering RoleSelectorView for {category_config.name} in {guild.id}: {e}"
)
# Register CustomColorButtonView (it's globally persistent by its custom_id)
try:
self.bot.add_view(CustomColorButtonView())
print(" Registered CustomColorButtonView globally.")
registered_count += 1
except Exception as e:
print(f" Error registering CustomColorButtonView globally: {e}")
print(
f"RoleSelectorCog: Finished registering {registered_count} persistent views."
)
def _get_guild_category_config(
self, guild_id: int, category_name_or_id: str
) -> Optional[GuildRoleCategoryConfig]:
configs = db.get_guild_role_category_configs(str(guild_id))
for config in configs:
if (
config.category_id == category_name_or_id
or config.name.lower() == category_name_or_id.lower()
):
return config
return None
async def autocomplete_category_name(
self, interaction: discord.Interaction, current: str
) -> List[app_commands.Choice[str]]:
choices = []
# Add existing guild category names
if interaction.guild:
guild_configs = db.get_guild_role_category_configs(
str(interaction.guild_id)
)
for config in guild_configs:
if config.name.lower().startswith(
current.lower()
): # Check if current is not empty before startswith
choices.append(
app_commands.Choice(name=config.name, value=config.name)
)
elif not current: # If current is empty, add all
choices.append(
app_commands.Choice(name=config.name, value=config.name)
)
# Add global preset names
presets = db.get_all_role_category_presets()
for preset in presets:
if preset.name.lower().startswith(
current.lower()
): # Check if current is not empty
choices.append(
app_commands.Choice(
name=f"Preset: {preset.name}", value=preset.name
)
)
elif not current: # If current is empty, add all
choices.append(
app_commands.Choice(
name=f"Preset: {preset.name}", value=preset.name
)
)
# Limit to 25 choices as per Discord API limits
return choices[:25]
@roleselect_group.command(
name="addcategory", description="Adds a new role category for selection."
)
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(
name="The name for the new category (or select a preset).",
description="A description for this role category.",
max_selectable="Maximum number of roles a user can select from this category (default: 1).",
preset_id="Optional ID of a global preset to base this category on (auto-filled if preset selected for name).",
)
@app_commands.autocomplete(name=autocomplete_category_name)
async def roleselect_addcategory(
self,
interaction: discord.Interaction,
name: str,
description: str,
max_selectable: Optional[int] = 1,
preset_id: Optional[str] = None,
):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
guild_id_str = str(interaction.guild_id)
# Ensure max_selectable has a valid default if None is passed by Discord for optional int
current_max_selectable = max_selectable if max_selectable is not None else 1
# Check if the provided 'name' is actually a preset name from autocomplete
# If it starts with "Preset: ", extract the actual preset name
actual_name = name
if name.startswith("Preset: "):
actual_name = name[len("Preset: ") :]
# If preset_id was not explicitly provided, set it to the ID of the selected preset
if not preset_id:
selected_preset = discord.utils.find(
lambda p: p.name == actual_name, db.get_all_role_category_presets()
)
if selected_preset:
preset_id = selected_preset.id
if (
self._get_guild_category_config(interaction.guild_id, actual_name)
and not preset_id
): # Allow adding preset even if name conflicts, preset name will be used
await interaction.response.send_message(
f"A custom role category named '{actual_name}' already exists.",
ephemeral=True,
)
return
roles_to_add: List[GuildRole] = []
is_preset_based = False
final_name = name
final_description = description
final_max_selectable = current_max_selectable
if preset_id:
preset = db.get_role_category_preset(preset_id)
if not preset:
await interaction.response.send_message(
f"Preset with ID '{preset_id}' not found.", ephemeral=True
)
return
final_name = preset.name # Use preset's name
if self._get_guild_category_config(
interaction.guild_id, final_name
): # Check if preset name already exists
await interaction.response.send_message(
f"A category based on preset '{final_name}' already exists.",
ephemeral=True,
)
return
# For auto-creating roles from preset
if not interaction.guild.me.guild_permissions.manage_roles:
await interaction.response.send_message(
"I need 'Manage Roles' permission to create roles from the preset.",
ephemeral=True,
)
return
# Define color map locally for this command, similar to init_defaults
color_map_for_creation = {
"Red": discord.Color.red(),
"Blue": discord.Color.blue(),
"Green": discord.Color.green(),
"Yellow": discord.Color.gold(),
"Purple": discord.Color.purple(),
"Orange": discord.Color.orange(),
"Pink": discord.Color.fuchsia(),
"Black": discord.Color(0x010101),
"White": discord.Color(0xFEFEFE),
}
# Defer if not already, as role creation can take time
if not interaction.response.is_done():
await interaction.response.defer(ephemeral=True, thinking=True)
created_roles_count = 0
for preset_role_option in preset.roles:
# Check if role with this NAME exists in the current guild
existing_role_in_guild = discord.utils.get(
interaction.guild.roles, name=preset_role_option.name
)
if existing_role_in_guild:
roles_to_add.append(
GuildRole(
role_id=str(existing_role_in_guild.id),
name=existing_role_in_guild.name,
emoji=preset_role_option.emoji,
)
)
else:
# Role does not exist by name, create it
role_color = discord.Color.default()
if (
preset.name.lower() == "colors"
and preset_role_option.name in color_map_for_creation
):
role_color = color_map_for_creation[preset_role_option.name]
try:
newly_created_role = await interaction.guild.create_role(
name=preset_role_option.name,
color=role_color,
permissions=discord.Permissions.none(), # Basic permissions
reason=f"Auto-created for preset '{preset.name}' by {interaction.user}",
)
roles_to_add.append(
GuildRole(
role_id=str(newly_created_role.id),
name=newly_created_role.name,
emoji=preset_role_option.emoji,
)
)
created_roles_count += 1
except discord.Forbidden:
await interaction.followup.send(
f"I lack permission to create the role '{preset_role_option.name}'. Skipping.",
ephemeral=True,
)
continue # Skip this role
except discord.HTTPException as e:
await interaction.followup.send(
f"Failed to create role '{preset_role_option.name}': {e}. Skipping.",
ephemeral=True,
)
continue # Skip this role
final_description = preset.description
final_max_selectable = preset.max_selectable
is_preset_based = True
new_config = GuildRoleCategoryConfig(
guild_id=guild_id_str,
name=final_name,
description=final_description,
roles=roles_to_add,
max_selectable=final_max_selectable,
is_preset=is_preset_based,
preset_id=preset_id if is_preset_based else None,
)
db.save_guild_role_category_config(new_config)
msg = f"Role category '{final_name}' added."
if is_preset_based:
msg += f" Based on preset '{preset_id}'."
else:
msg += f" Use `/roleselect addrole category_name_or_id:{final_name} role:<role> [emoji:<emoji>]` to add roles." # Updated help text
msg += f" Then use `/roleselect post category_name_or_id:{final_name} channel:<#channel>` to post." # Updated help text
if interaction.response.is_done():
await interaction.followup.send(msg, ephemeral=True)
else:
await interaction.response.send_message(msg, ephemeral=True)
@roleselect_group.command(
name="removecategory",
description="Removes a role category and its selector message.",
)
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(
category_name_or_id="The name or ID of the category to remove."
)
async def roleselect_removecategory(
self, interaction: discord.Interaction, category_name_or_id: str
):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
guild_id = interaction.guild_id
config_to_remove = self._get_guild_category_config(
guild_id, category_name_or_id
)
if not config_to_remove:
await interaction.response.send_message(
f"Role category '{category_name_or_id}' not found.", ephemeral=True
)
return
deleted_message_feedback = ""
if config_to_remove.message_id and config_to_remove.channel_id:
try:
channel = interaction.guild.get_channel(
int(config_to_remove.channel_id)
)
if channel and isinstance(channel, discord.TextChannel):
message = await channel.fetch_message(
int(config_to_remove.message_id)
)
await message.delete()
deleted_message_feedback = (
f" Deleted selector message for '{config_to_remove.name}'."
)
except Exception as e:
deleted_message_feedback = f" Could not delete selector message: {e}."
db.delete_guild_role_category_config(
str(guild_id), config_to_remove.category_id
)
response_message = f"Role category '{config_to_remove.name}' removed.{deleted_message_feedback}"
if interaction.response.is_done():
await interaction.followup.send(response_message, ephemeral=True)
else:
await interaction.response.send_message(response_message, ephemeral=True)
@roleselect_group.command(
name="listcategories",
description="Lists all configured role selection categories.",
)
@app_commands.checks.has_permissions(manage_guild=True)
async def roleselect_listcategories(self, interaction: discord.Interaction):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
guild_id_str = str(interaction.guild_id)
configs = db.get_guild_role_category_configs(guild_id_str)
if not configs:
await interaction.response.send_message(
"No role selection categories configured.", ephemeral=True
)
return
embed = discord.Embed(
title="Configured Role Selection Categories", color=discord.Color.blue()
)
for config in configs:
roles_str = ", ".join([r.name for r in config.roles[:5]]) + (
"..." if len(config.roles) > 5 else ""
)
embed.add_field(
name=f"{config.name} (ID: `{config.category_id}`)",
value=f"Desc: {config.description}\nMax: {config.max_selectable}\nRoles: {roles_str or 'None'}\nPreset: {config.preset_id or 'No'}",
inline=False,
)
await interaction.response.send_message(
embed=embed, ephemeral=False
) # Make it visible
@roleselect_group.command(
name="listpresets",
description="Lists all available global role category presets.",
)
async def roleselect_listpresets(self, interaction: discord.Interaction):
presets = db.get_all_role_category_presets()
if not presets:
await interaction.response.send_message(
"No global presets available.", ephemeral=True
)
return
embed = discord.Embed(
title="Available Role Category Presets", color=discord.Color.green()
)
for preset in sorted(presets, key=lambda p: p.display_order):
roles_str = ", ".join(
[f"{r.name} ({r.role_id})" for r in preset.roles[:3]]
) + ("..." if len(preset.roles) > 3 else "")
embed.add_field(
name=f"{preset.name} (ID: `{preset.id}`)",
value=f"Desc: {preset.description}\nMax: {preset.max_selectable}\nRoles: {roles_str or 'None'}",
inline=False,
)
await interaction.response.send_message(
embed=embed, ephemeral=False
) # Make it visible
@roleselect_group.command(
name="addrole", description="Adds a role to a specified category."
)
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(
category_name_or_id="The name or ID of the category to add the role to.",
role="The role to add.",
emoji="Optional emoji to display next to the role in the selector.",
)
async def roleselect_addrole(
self,
interaction: discord.Interaction,
category_name_or_id: str,
role: discord.Role,
emoji: Optional[str] = None,
):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
guild_id = interaction.guild_id
config = self._get_guild_category_config(guild_id, category_name_or_id)
if not config:
await interaction.response.send_message(
f"Category '{category_name_or_id}' not found.", ephemeral=True
)
return
if config.is_preset:
await interaction.response.send_message(
f"Category '{config.name}' uses a preset. Roles are managed via the preset definition.",
ephemeral=True,
)
return
if any(r.role_id == str(role.id) for r in config.roles):
await interaction.response.send_message(
f"Role '{role.name}' is already in '{config.name}'.", ephemeral=True
)
return
config.roles.append(
GuildRole(role_id=str(role.id), name=role.name, emoji=emoji)
)
db.save_guild_role_category_config(config)
await interaction.response.send_message(
f"Role '{role.name}' added to '{config.name}'.", ephemeral=True
)
@roleselect_group.command(
name="removerole", description="Removes a role from a specified category."
)
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(
category_name_or_id="The name or ID of the category to remove the role from.",
role="The role to remove.",
)
async def roleselect_removerole(
self,
interaction: discord.Interaction,
category_name_or_id: str,
role: discord.Role,
):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
guild_id = interaction.guild_id
config = self._get_guild_category_config(guild_id, category_name_or_id)
if not config:
await interaction.response.send_message(
f"Category '{category_name_or_id}' not found.", ephemeral=True
)
return
if config.is_preset:
await interaction.response.send_message(
f"Category '{config.name}' uses a preset. Roles are managed via the preset definition.",
ephemeral=True,
)
return
initial_len = len(config.roles)
config.roles = [r for r in config.roles if r.role_id != str(role.id)]
if len(config.roles) < initial_len:
db.save_guild_role_category_config(config)
await interaction.response.send_message(
f"Role '{role.name}' removed from '{config.name}'.", ephemeral=True
)
else:
await interaction.response.send_message(
f"Role '{role.name}' not found in '{config.name}'.", ephemeral=True
)
@roleselect_group.command(
name="setcolorui",
description="Posts the UI for users to set their custom role color.",
)
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(
channel="The channel to post the custom color UI in (defaults to current channel)."
)
async def roleselect_setcolorui(
self,
interaction: discord.Interaction,
channel: Optional[discord.TextChannel] = None,
):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
target_channel = channel or interaction.channel
if not isinstance(
target_channel, discord.TextChannel
): # Ensure it's a text channel
await interaction.response.send_message(
"Invalid channel specified for custom color UI.", ephemeral=True
)
return
embed = discord.Embed(
title="🎨 Custom Role Color",
description="Click the button below to set a custom color for your name in this server!",
color=discord.Color.random(),
)
view = CustomColorButtonView()
try:
await target_channel.send(embed=embed, view=view)
if target_channel != interaction.channel:
await interaction.response.send_message(
f"Custom color button posted in {target_channel.mention}.",
ephemeral=True,
)
else:
await interaction.response.send_message(
"Custom color button posted.", ephemeral=True
)
except discord.Forbidden:
await interaction.response.send_message(
f"I don't have permissions to send messages in {target_channel.mention}.",
ephemeral=True,
)
except Exception as e:
await interaction.response.send_message(
f"Error posting custom color button: {e}", ephemeral=True
)
@roleselect_group.command(
name="post",
description="Posts or updates the role selector message for a category.",
)
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(
category_name_or_id="The name or ID of the category to post.",
channel="The channel to post the selector in (defaults to current channel).",
)
async def roleselect_post(
self,
interaction: discord.Interaction,
category_name_or_id: str,
channel: Optional[discord.TextChannel] = None,
):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
target_channel = channel or interaction.channel
if not isinstance(
target_channel, discord.TextChannel
): # Ensure it's a text channel
await interaction.response.send_message(
"Invalid channel specified for posting.", ephemeral=True
)
return
guild = interaction.guild
config = self._get_guild_category_config(guild.id, category_name_or_id)
if not config:
await interaction.response.send_message(
f"Category '{category_name_or_id}' not found.", ephemeral=True
)
return
if not config.roles:
await interaction.response.send_message(
f"Category '{config.name}' has no roles. Add roles first using `/roleselect addrole`.",
ephemeral=True,
)
return
embed = discord.Embed(
title=f"{config.name} Roles ✨",
description=config.description,
color=discord.Color.blue(),
)
view = RoleSelectorView(guild.id, config, self.bot)
# Defer the response as message fetching/editing can take time
await interaction.response.defer(ephemeral=True)
if config.message_id and config.channel_id:
try:
original_channel = guild.get_channel(int(config.channel_id))
if original_channel and isinstance(
original_channel, discord.TextChannel
):
message_to_edit = await original_channel.fetch_message(
int(config.message_id)
)
await message_to_edit.edit(embed=embed, view=view)
if original_channel.id != target_channel.id: # Moved
# Delete old message, post new one, then update config
await message_to_edit.delete() # Delete the old message first
new_msg = await target_channel.send(embed=embed, view=view)
config.channel_id = str(target_channel.id)
config.message_id = str(new_msg.id)
await interaction.followup.send(
f"Updated and moved selector for '{config.name}' to {target_channel.mention}.",
ephemeral=True,
)
else: # Just updated in the same channel
await interaction.followup.send(
f"Updated selector for '{config.name}' in {target_channel.mention}.",
ephemeral=True,
)
db.save_guild_role_category_config(config)
return
except Exception as e: # NotFound, Forbidden, etc.
await interaction.followup.send(
f"Couldn't update original message ({e}), posting new one.",
ephemeral=True,
)
config.message_id = None
config.channel_id = None # Clear old IDs as we are posting a new one
try:
msg = await target_channel.send(embed=embed, view=view)
config.message_id = str(msg.id)
config.channel_id = str(target_channel.id)
db.save_guild_role_category_config(config)
await interaction.followup.send(
f"Posted role selector for '{config.name}' in {target_channel.mention}.",
ephemeral=True,
)
except Exception as e:
await interaction.followup.send(
f"Error posting role selector: {e}", ephemeral=True
)
@roleselect_group.command(
name="postallcategories",
description="Posts or updates selector messages for all categories in a channel.",
)
@app_commands.checks.has_permissions(manage_guild=True)
@app_commands.describe(
channel="The channel to post all selectors in (defaults to current channel)."
)
async def roleselect_postallcategories(
self,
interaction: discord.Interaction,
channel: Optional[discord.TextChannel] = None,
):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
target_channel = channel or interaction.channel
if not isinstance(target_channel, discord.TextChannel):
await interaction.response.send_message(
"Invalid channel specified for posting.", ephemeral=True
)
return
guild = interaction.guild
all_guild_configs = db.get_guild_role_category_configs(str(guild.id))
if not all_guild_configs:
await interaction.response.send_message(
"No role categories configured for this server.", ephemeral=True
)
return
await interaction.response.defer(ephemeral=True, thinking=True)
posted_count = 0
updated_count = 0
skipped_count = 0
error_count = 0
feedback_details = []
for config in all_guild_configs:
if not config.roles:
feedback_details.append(
f"Skipped '{config.name}': No roles configured."
)
skipped_count += 1
continue
embed = discord.Embed(
title=f"{config.name} Roles ✨",
description=config.description,
color=discord.Color.blue(),
)
view = RoleSelectorView(guild.id, config, self.bot)
action_taken = "Posted"
# Check if a message already exists and handle it
if config.message_id and config.channel_id:
try:
original_channel = guild.get_channel(int(config.channel_id))
if original_channel and isinstance(
original_channel, discord.TextChannel
):
message_to_edit_or_delete = (
await original_channel.fetch_message(int(config.message_id))
)
if (
original_channel.id != target_channel.id
): # Moving to a new channel
await message_to_edit_or_delete.delete() # Delete from old location
# message_id and channel_id will be updated when new message is sent
config.message_id = None
config.channel_id = None
action_taken = f"Moved '{config.name}' to {target_channel.mention} and posted"
else: # Updating in the same channel (target_channel is the original_channel)
await message_to_edit_or_delete.edit(embed=embed, view=view)
db.save_guild_role_category_config(
config
) # Save updated config (no change to message/channel id)
feedback_details.append(
f"Updated selector for '{config.name}' in {target_channel.mention}."
)
updated_count += 1
continue # Skip to next config as this one is handled
except discord.NotFound:
feedback_details.append(
f"Note: Original message for '{config.name}' not found, will post anew."
)
config.message_id = None
config.channel_id = None
except discord.Forbidden:
feedback_details.append(
f"Error: Could not access/delete original message for '{config.name}' in {original_channel.mention if original_channel else 'unknown channel'}. Posting new one."
)
config.message_id = None
config.channel_id = None
except Exception as e:
feedback_details.append(
f"Error handling original message for '{config.name}': {e}. Posting new one."
)
config.message_id = None
config.channel_id = None
# Post new message if no existing one or if it was deleted due to move
try:
new_msg = await target_channel.send(embed=embed, view=view)
config.message_id = str(new_msg.id)
config.channel_id = str(target_channel.id)
db.save_guild_role_category_config(config)
feedback_details.append(
f"{action_taken} selector for '{config.name}' in {target_channel.mention}."
)
if action_taken.startswith("Moved") or action_taken.startswith(
"Posted"
):
posted_count += 1
else: # Should be covered by updated_count already
pass
except discord.Forbidden:
feedback_details.append(
f"Error: No permission to post '{config.name}' in {target_channel.mention}."
)
error_count += 1
except Exception as e:
feedback_details.append(f"Error posting '{config.name}': {e}.")
error_count += 1
summary_message = (
f"Finished posting all category selectors to {target_channel.mention}.\n"
f"Successfully Posted/Moved: {posted_count}\n"
f"Successfully Updated: {updated_count}\n"
f"Skipped (no roles/already exists): {skipped_count}\n"
f"Errors: {error_count}\n\n"
"Details:\n" + "\n".join(feedback_details)
)
await interaction.followup.send(summary_message, ephemeral=True)
# Role Preset Commands (Owner Only)
@rolepreset_group.command(
name="add", description="Creates a new global role category preset."
)
@app_commands.check(is_owner_check)
@app_commands.describe(
preset_id="A unique ID for this preset (e.g., 'color_roles', 'region_roles').",
name="The display name for this preset.",
description="A description for this preset.",
max_selectable="Maximum roles a user can select from categories using this preset (default: 1).",
display_order="Order in which this preset appears in lists (lower numbers first, default: 0).",
)
async def rolepreset_add(
self,
interaction: discord.Interaction,
preset_id: str,
name: str,
description: str,
max_selectable: Optional[int] = 1,
display_order: Optional[int] = 0,
):
current_max_selectable = max_selectable if max_selectable is not None else 1
current_display_order = display_order if display_order is not None else 0
if db.get_role_category_preset(preset_id):
await interaction.response.send_message(
f"Preset ID '{preset_id}' already exists.", ephemeral=True
)
return
new_preset = RoleCategoryPreset(
id=preset_id,
name=name,
description=description,
roles=[],
max_selectable=current_max_selectable,
display_order=current_display_order,
)
db.save_role_category_preset(new_preset)
await interaction.response.send_message(
f"Preset '{name}' (ID: {preset_id}) created. Add roles with `/rolepreset addrole`.",
ephemeral=True,
)
@rolepreset_group.command(
name="remove", description="Removes a global role category preset."
)
@app_commands.check(is_owner_check)
@app_commands.describe(preset_id="The ID of the preset to remove.")
async def rolepreset_remove(self, interaction: discord.Interaction, preset_id: str):
if not db.get_role_category_preset(preset_id):
await interaction.response.send_message(
f"Preset ID '{preset_id}' not found.", ephemeral=True
)
return
db.delete_role_category_preset(preset_id)
await interaction.response.send_message(
f"Preset ID '{preset_id}' removed.", ephemeral=True
)
@rolepreset_group.command(
name="addrole", description="Adds a role (by ID or name) to a global preset."
)
@app_commands.check(is_owner_check)
@app_commands.describe(
preset_id="The ID of the preset to add the role to.",
role_name_or_id="The name or ID of the role to add. The first matching role found across all servers the bot is in will be used.",
emoji="Optional emoji for the role in this preset.",
)
async def rolepreset_addrole(
self,
interaction: discord.Interaction,
preset_id: str,
role_name_or_id: str,
emoji: Optional[str] = None,
):
preset = db.get_role_category_preset(preset_id)
if not preset:
await interaction.response.send_message(
f"Preset ID '{preset_id}' not found.", ephemeral=True
)
return
target_role: Optional[discord.Role] = None
role_display_name = role_name_or_id
# Attempt to find role by ID first, then by name across all guilds
try:
role_id_int = int(role_name_or_id)
for guild in self.bot.guilds:
if r := guild.get_role(role_id_int):
target_role = r
role_display_name = r.name
break
except ValueError: # Not an ID, try by name
for guild in self.bot.guilds:
for r_obj in guild.roles:
if r_obj.name.lower() == role_name_or_id.lower():
target_role = r_obj
role_display_name = r_obj.name
break
if target_role:
break
if not target_role:
await interaction.response.send_message(
f"Role '{role_name_or_id}' not found in any server the bot is in.",
ephemeral=True,
)
return
if any(r.role_id == str(target_role.id) for r in preset.roles):
await interaction.response.send_message(
f"Role '{target_role.name}' (ID: {target_role.id}) is already in preset '{preset.name}'.",
ephemeral=True,
)
return
preset.roles.append(
RoleOption(role_id=str(target_role.id), name=role_display_name, emoji=emoji)
)
db.save_role_category_preset(preset)
await interaction.response.send_message(
f"Role '{role_display_name}' (ID: {target_role.id}) added to preset '{preset.name}'.",
ephemeral=True,
)
@rolepreset_group.command(
name="removerole",
description="Removes a role (by ID or name) from a global preset.",
)
@app_commands.check(is_owner_check)
@app_commands.describe(
preset_id="The ID of the preset to remove the role from.",
role_id_or_name="The ID or name of the role to remove from the preset.",
)
async def rolepreset_removerole(
self, interaction: discord.Interaction, preset_id: str, role_id_or_name: str
):
preset = db.get_role_category_preset(preset_id)
if not preset:
await interaction.response.send_message(
f"Preset ID '{preset_id}' not found.", ephemeral=True
)
return
initial_len = len(preset.roles)
# Try to match by ID first, then by name if ID doesn't match or isn't an int
role_to_remove_id_str: Optional[str] = None
try:
role_id_int = int(role_id_or_name)
role_to_remove_id_str = str(role_id_int)
except ValueError:
pass # role_id_or_name is not an integer, will try to match by name
if role_to_remove_id_str:
preset.roles = [
r for r in preset.roles if r.role_id != role_to_remove_id_str
]
else: # Match by name (case-insensitive)
preset.roles = [
r for r in preset.roles if r.name.lower() != role_id_or_name.lower()
]
if len(preset.roles) < initial_len:
db.save_role_category_preset(preset)
await interaction.response.send_message(
f"Role matching '{role_id_or_name}' removed from preset '{preset.name}'.",
ephemeral=True,
)
else:
await interaction.response.send_message(
f"Role matching '{role_id_or_name}' not found in preset '{preset.name}'.",
ephemeral=True,
)
@rolepreset_group.command(
name="add_all_to_guild",
description="Adds all available global presets as categories to this guild.",
)
@app_commands.checks.has_permissions(manage_guild=True)
async def rolepreset_add_all_to_guild(self, interaction: discord.Interaction):
if not interaction.guild:
await interaction.response.send_message(
"This command can only be used in a server.", ephemeral=True
)
return
if not interaction.guild.me.guild_permissions.manage_roles:
await interaction.response.send_message(
"I need 'Manage Roles' permission to create roles from presets.",
ephemeral=True,
)
return
await interaction.response.defer(ephemeral=True, thinking=True)
guild_id_str = str(interaction.guild_id)
all_presets = db.get_all_role_category_presets()
added_count = 0
skipped_count = 0
feedback_messages = []
color_map_for_creation = {
"Red": discord.Color.red(),
"Blue": discord.Color.blue(),
"Green": discord.Color.green(),
"Yellow": discord.Color.gold(),
"Purple": discord.Color.purple(),
"Orange": discord.Color.orange(),
"Pink": discord.Color.fuchsia(),
"Black": discord.Color(0x010101),
"White": discord.Color(0xFEFEFE),
}
for preset in all_presets:
# Check if a category based on this preset already exists in the guild
if self._get_guild_category_config(interaction.guild_id, preset.name):
feedback_messages.append(
f"Skipped '{preset.name}': A category with this name already exists."
)
skipped_count += 1
continue
roles_to_add: List[GuildRole] = []
for preset_role_option in preset.roles:
existing_role_in_guild = discord.utils.get(
interaction.guild.roles, name=preset_role_option.name
)
if existing_role_in_guild:
roles_to_add.append(
GuildRole(
role_id=str(existing_role_in_guild.id),
name=existing_role_in_guild.name,
emoji=preset_role_option.emoji,
)
)
else:
role_color = discord.Color.default()
if (
preset.name.lower() == "colors"
and preset_role_option.name in color_map_for_creation
):
role_color = color_map_for_creation[preset_role_option.name]
try:
newly_created_role = await interaction.guild.create_role(
name=preset_role_option.name,
color=role_color,
permissions=discord.Permissions.none(),
reason=f"Auto-created for preset '{preset.name}' by {interaction.user}",
)
roles_to_add.append(
GuildRole(
role_id=str(newly_created_role.id),
name=newly_created_role.name,
emoji=preset_role_option.emoji,
)
)
except discord.Forbidden:
feedback_messages.append(
f"Warning: Couldn't create role '{preset_role_option.name}' for preset '{preset.name}' due to permissions."
)
continue
except discord.HTTPException as e:
feedback_messages.append(
f"Warning: Failed to create role '{preset_role_option.name}' for preset '{preset.name}': {e}."
)
continue
new_config = GuildRoleCategoryConfig(
guild_id=guild_id_str,
name=preset.name,
description=preset.description,
roles=roles_to_add,
max_selectable=preset.max_selectable,
is_preset=True,
preset_id=preset.id,
)
db.save_guild_role_category_config(new_config)
feedback_messages.append(f"Added '{preset.name}' as a new category.")
added_count += 1
final_message = (
f"Attempted to add {len(all_presets)} presets.\nAdded: {added_count}\nSkipped: {skipped_count}\n\nDetails:\n"
+ "\n".join(feedback_messages)
)
await interaction.followup.send(final_message, ephemeral=True)
@rolepreset_group.command(
name="init_defaults",
description="Initializes default global presets based on role_creator_cog structure.",
)
@app_commands.check(is_owner_check)
async def rolepreset_init_defaults(self, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True, thinking=True)
# Definitions from role_creator_cog.py
color_map_creator = {
"Red": discord.Color.red(),
"Blue": discord.Color.blue(),
"Green": discord.Color.green(),
"Yellow": discord.Color.gold(),
"Purple": discord.Color.purple(),
"Orange": discord.Color.orange(),
"Pink": discord.Color.fuchsia(),
"Black": discord.Color(0x010101),
"White": discord.Color(0xFEFEFE),
}
role_categories_creator = {
"Colors": {
"roles": [
"Red",
"Blue",
"Green",
"Yellow",
"Purple",
"Orange",
"Pink",
"Black",
"White",
],
"max": 1,
"desc": "Choose your favorite color role.",
},
"Regions": {
"roles": [
"NA East",
"NA West",
"EU",
"Asia",
"Oceania",
"South America",
],
"max": 1,
"desc": "Select your region.",
},
"Pronouns": {
"roles": ["He/Him", "She/Her", "They/Them", "Ask Pronouns"],
"max": 4,
"desc": "Select your pronoun roles.",
},
"Interests": {
"roles": [
"Art",
"Music",
"Movies",
"Books",
"Technology",
"Science",
"History",
"Food",
"Programming",
"Anime",
"Photography",
"Travel",
"Writing",
"Cooking",
"Fitness",
"Nature",
"Gaming",
"Philosophy",
"Psychology",
"Design",
"Machine Learning",
"Cryptocurrency",
"Astronomy",
"Mythology",
"Languages",
"Architecture",
"DIY Projects",
"Hiking",
"Streaming",
"Virtual Reality",
"Coding Challenges",
"Board Games",
"Meditation",
"Urban Exploration",
"Tattoo Art",
"Comics",
"Robotics",
"3D Modeling",
"Podcasts",
],
"max": 16,
"desc": "Select your interests.",
},
"Gaming Platforms": {
"roles": ["PC", "PlayStation", "Xbox", "Nintendo Switch", "Mobile"],
"max": 5,
"desc": "Select your gaming platforms.",
},
"Favorite Vocaloids": {
"roles": [
"Hatsune Miku",
"Kasane Teto",
"Akita Neru",
"Kagamine Rin",
"Kagamine Len",
"Megurine Luka",
"Kaito",
"Meiko",
"Gumi",
"Kaai Yuki",
"Adachi Rei",
],
"max": 10,
"desc": "Select your favorite Vocaloids.",
},
"Notifications": {
"roles": ["Announcements"],
"max": 1,
"desc": "Opt-in for announcements.",
},
}
created_presets = 0
skipped_presets = 0
preset_details_msg = ""
for idx, (category_name, cat_details) in enumerate(
role_categories_creator.items()
):
preset_id = f"default_{category_name.lower().replace(' ', '_')}"
if db.get_role_category_preset(preset_id):
preset_details_msg += f"Skipped: Preset '{category_name}' (ID: {preset_id}) already exists.\n"
skipped_presets += 1
continue
role_options_for_preset: List[RoleOption] = []
for role_name_in_creator in cat_details["roles"]:
# Find a canonical role ID from any guild for this role name
# This is a bit naive as role names might not be unique or exist
# The `/rolepreset addrole` command does this search more robustly.
# For init_defaults, we'll store the name and expect `/roleselect addcategory` to handle creation.
# The RoleOption model requires a role_id. We can use a placeholder or a convention.
# For now, let's use the role name as a placeholder for role_id if no actual ID is found,
# or better, skip if no role is found to ensure preset integrity.
# The user's request was to use what's in role_creator_cog.
# The role_creator_cog *creates* these roles.
# The preset system should ideally reference existing roles or define names for creation.
# Let's simplify: the preset will store the *name*. The guild-specific setup will create it.
# The RoleOption model has role_id and name. For presets, role_id might be less critical
# if the expectation is that the guild-level command creates roles by name.
# However, the current `/rolepreset addrole` finds an existing role ID.
# To be consistent, `init_defaults` should also try to find an ID.
found_role_for_option: Optional[discord.Role] = None
for g in self.bot.guilds:
for r in g.roles:
if r.name.lower() == role_name_in_creator.lower():
found_role_for_option = r
break
if found_role_for_option:
break
if found_role_for_option:
role_options_for_preset.append(
RoleOption(
role_id=str(
found_role_for_option.id
), # Use ID of first found role
name=role_name_in_creator, # Use the canonical name from creator_cog
emoji=None, # No emojis in role_creator_cog
)
)
else:
# If no role found across all guilds, we can't create a valid RoleOption for the preset.
# We could store the name as a placeholder, but this deviates from RoleOption model.
# For now, we'll log this and skip adding this specific role to the preset.
# The owner can add it manually later if needed.
preset_details_msg += f"Warning: Role '{role_name_in_creator}' for preset '{category_name}' not found in any guild. It won't be added to the preset.\n"
new_preset = RoleCategoryPreset(
id=preset_id,
name=category_name,
description=cat_details["desc"],
roles=role_options_for_preset,
max_selectable=cat_details["max"],
display_order=idx,
)
db.save_role_category_preset(new_preset)
created_presets += 1
preset_details_msg += f"Created: Preset '{category_name}' (ID: {preset_id}) with {len(role_options_for_preset)} role options.\n"
final_summary = f"Default preset initialization complete.\nCreated: {created_presets}\nSkipped: {skipped_presets}\n\nDetails:\n{preset_details_msg}"
await interaction.followup.send(final_summary, ephemeral=True)
# Deprecated commands are removed as they are not slash commands and functionality is covered
async def setup(bot):
cog = RoleSelectorCog(bot)
await bot.add_cog(cog)
# Syncing should ideally happen once after all cogs are loaded, e.g., in main.py or a central setup.
# If this cog is reloaded, syncing here might be okay.
# For now, let's assume global sync happens elsewhere or is handled by bot.setup_hook if it calls tree.sync
# await bot.tree.sync()
print(
"RoleSelectorCog loaded. Persistent views will be registered. Ensure slash commands are synced globally if needed."
)