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: [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." )