discordbot/cogs/role_selector_cog.py

655 lines
34 KiB
Python

import discord
from discord.ext import 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
)
# 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)
await interaction.followup.send(f"Your custom role color has been set to {user_color_role_data.hex_color}!", 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 = [], [], []
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)}"
if not added_names and not removed_names:
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):
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
@commands.group(name="roleselect", invoke_without_command=True)
@commands.has_permissions(manage_guild=True)
async def roleselect(self, ctx: commands.Context):
await ctx.send_help(ctx.command)
@roleselect.command(name="addcategory")
@commands.has_permissions(manage_guild=True)
async def roleselect_addcategory(self, ctx: commands.Context, name: str, description: str, max_selectable: int = 1, preset_id: Optional[str] = None):
guild_id_str = str(ctx.guild.id)
if self._get_guild_category_config(ctx.guild.id, name) and not preset_id: # Allow adding preset even if name conflicts, preset name will be used
await ctx.send(f"A custom role category named '{name}' already exists.")
return
roles_to_add: List[GuildRole] = []
is_preset_based = False
final_name = name
final_description = description
final_max_selectable = max_selectable
if preset_id:
preset = db.get_role_category_preset(preset_id)
if not preset:
await ctx.send(f"Preset with ID '{preset_id}' not found.")
return
final_name = preset.name # Use preset's name
if self._get_guild_category_config(ctx.guild.id, final_name): # Check if preset name already exists
await ctx.send(f"A category based on preset '{final_name}' already exists.")
return
for preset_role_option in preset.roles:
role_in_guild = ctx.guild.get_role(int(preset_role_option.role_id))
if not role_in_guild:
await ctx.send(f"Warning: Role '{preset_role_option.name}' (ID: {preset_role_option.role_id}) from preset not found in this server. Skipping.")
continue
roles_to_add.append(GuildRole(role_id=str(role_in_guild.id), name=role_in_guild.name, emoji=preset_role_option.emoji))
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 \"{final_name}\" <role> [emoji]` to add roles."
msg += f" Then use `!roleselect post \"{final_name}\" #channel` to post."
await ctx.send(msg)
@roleselect.command(name="removecategory")
@commands.has_permissions(manage_guild=True)
async def roleselect_removecategory(self, ctx: commands.Context, category_name_or_id: str):
guild_id = ctx.guild.id
config_to_remove = self._get_guild_category_config(guild_id, category_name_or_id)
if not config_to_remove:
await ctx.send(f"Role category '{category_name_or_id}' not found.")
return
if config_to_remove.message_id and config_to_remove.channel_id:
try:
channel = ctx.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()
await ctx.send(f"Deleted selector message for '{config_to_remove.name}'.")
except Exception as e:
await ctx.send(f"Could not delete selector message: {e}")
db.delete_guild_role_category_config(str(guild_id), config_to_remove.category_id)
await ctx.send(f"Role category '{config_to_remove.name}' removed.")
@roleselect.command(name="listcategories")
@commands.has_permissions(manage_guild=True)
async def roleselect_listcategories(self, ctx: commands.Context):
guild_id_str = str(ctx.guild.id)
configs = db.get_guild_role_category_configs(guild_id_str)
if not configs:
await ctx.send("No role selection categories configured.")
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 ctx.send(embed=embed)
@roleselect.command(name="listpresets")
@commands.is_owner() # Presets are global, so owner only
async def roleselect_listpresets(self, ctx: commands.Context):
presets = db.get_all_role_category_presets()
if not presets:
await ctx.send("No global presets available.")
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 ctx.send(embed=embed)
@roleselect.command(name="addrole")
@commands.has_permissions(manage_guild=True)
async def roleselect_addrole(self, ctx: commands.Context, category_name_or_id: str, role: discord.Role, emoji: Optional[str] = None):
guild_id = ctx.guild.id
config = self._get_guild_category_config(guild_id, category_name_or_id)
if not config:
await ctx.send(f"Category '{category_name_or_id}' not found.")
return
if config.is_preset:
await ctx.send(f"Category '{config.name}' uses a preset. Roles are managed via the preset definition.")
return
if any(r.role_id == str(role.id) for r in config.roles):
await ctx.send(f"Role '{role.name}' is already in '{config.name}'.")
return
config.roles.append(GuildRole(role_id=str(role.id), name=role.name, emoji=emoji))
db.save_guild_role_category_config(config)
await ctx.send(f"Role '{role.name}' added to '{config.name}'.")
@roleselect.command(name="removerole")
@commands.has_permissions(manage_guild=True)
async def roleselect_removerole(self, ctx: commands.Context, category_name_or_id: str, role: discord.Role):
guild_id = ctx.guild.id
config = self._get_guild_category_config(guild_id, category_name_or_id)
if not config:
await ctx.send(f"Category '{category_name_or_id}' not found.")
return
if config.is_preset:
await ctx.send(f"Category '{config.name}' uses a preset. Roles are managed via the preset definition.")
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 ctx.send(f"Role '{role.name}' removed from '{config.name}'.")
else:
await ctx.send(f"Role '{role.name}' not found in '{config.name}'.")
@roleselect.command(name="setcolorui")
@commands.has_permissions(manage_guild=True)
async def roleselect_setcolorui(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
target_channel = channel or ctx.channel
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 != ctx.channel:
await ctx.send(f"Custom color button posted in {target_channel.mention}.", ephemeral=True)
except discord.Forbidden:
await ctx.send(f"I don't have permissions to send messages in {target_channel.mention}.", ephemeral=True)
except Exception as e:
await ctx.send(f"Error posting custom color button: {e}", ephemeral=True)
@roleselect.command(name="post")
@commands.has_permissions(manage_guild=True)
async def roleselect_post(self, ctx: commands.Context, category_name_or_id: str, channel: Optional[discord.TextChannel] = None):
target_channel = channel or ctx.channel
guild = ctx.guild
config = self._get_guild_category_config(guild.id, category_name_or_id)
if not config:
await ctx.send(f"Category '{category_name_or_id}' not found.")
return
if not config.roles:
await ctx.send(f"Category '{config.name}' has no roles. Add roles first.")
return
embed = discord.Embed(title=f"{config.name} Roles ✨", description=config.description, color=discord.Color.blue())
view = RoleSelectorView(guild.id, config, self.bot)
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
config.channel_id = str(target_channel.id)
new_msg = await target_channel.send(embed=embed, view=view)
config.message_id = str(new_msg.id)
await message_to_edit.delete()
await ctx.send(f"Updated and moved selector for '{config.name}' to {target_channel.mention}.")
else: # Just updated
await ctx.send(f"Updated selector for '{config.name}' in {target_channel.mention}.")
db.save_guild_role_category_config(config)
return
except Exception as e: # NotFound, Forbidden, etc.
await ctx.send(f"Couldn't update original message ({e}), posting new one.")
config.message_id = None; config.channel_id = None # Clear old IDs
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 ctx.send(f"Posted role selector for '{config.name}' in {target_channel.mention}.")
except Exception as e:
await ctx.send(f"Error posting role selector: {e}")
@commands.group(name="rolepreset", invoke_without_command=True)
@commands.is_owner()
async def rolepreset(self, ctx: commands.Context):
await ctx.send_help(ctx.command)
@rolepreset.command(name="add")
@commands.is_owner()
async def rolepreset_add(self, ctx: commands.Context, preset_id: str, name: str, description: str, max_selectable: int = 1, display_order: int = 0):
if db.get_role_category_preset(preset_id):
await ctx.send(f"Preset ID '{preset_id}' already exists.")
return
new_preset = RoleCategoryPreset(id=preset_id, name=name, description=description, roles=[], max_selectable=max_selectable, display_order=display_order)
db.save_role_category_preset(new_preset)
await ctx.send(f"Preset '{name}' (ID: {preset_id}) created. Add roles with `!rolepreset addrole`.")
@rolepreset.command(name="remove")
@commands.is_owner()
async def rolepreset_remove(self, ctx: commands.Context, preset_id: str):
if not db.get_role_category_preset(preset_id):
await ctx.send(f"Preset ID '{preset_id}' not found.")
return
db.delete_role_category_preset(preset_id)
await ctx.send(f"Preset ID '{preset_id}' removed.")
@rolepreset.command(name="addrole")
@commands.is_owner()
async def rolepreset_addrole(self, ctx: commands.Context, preset_id: str, role_name_or_id: str, emoji: Optional[str] = None):
preset = db.get_role_category_preset(preset_id)
if not preset:
await ctx.send(f"Preset ID '{preset_id}' not found.")
return
target_role: Optional[discord.Role] = None
role_display_name = role_name_or_id
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:
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 ctx.send(f"Role '{role_name_or_id}' not found in any server.")
return
if any(r.role_id == str(target_role.id) for r in preset.roles):
await ctx.send(f"Role '{target_role.name}' already in preset '{preset.name}'.")
return
preset.roles.append(RoleOption(role_id=str(target_role.id), name=role_display_name, emoji=emoji))
db.save_role_category_preset(preset)
await ctx.send(f"Role '{role_display_name}' added to preset '{preset.name}'.")
@rolepreset.command(name="removerole")
@commands.is_owner()
async def rolepreset_removerole(self, ctx: commands.Context, preset_id: str, role_id_or_name: str):
preset = db.get_role_category_preset(preset_id)
if not preset:
await ctx.send(f"Preset ID '{preset_id}' not found.")
return
initial_len = len(preset.roles)
preset.roles = [r for r in preset.roles if not (r.role_id == role_id_or_name or r.name.lower() == role_id_or_name.lower())]
if len(preset.roles) < initial_len:
db.save_role_category_preset(preset)
await ctx.send(f"Role matching '{role_id_or_name}' removed from preset '{preset.name}'.")
else:
await ctx.send(f"Role matching '{role_id_or_name}' not found in preset.")
@commands.command(name="create_role_embeds", hidden=True)
@commands.is_owner()
async def create_role_embeds_old(self, ctx: commands.Context):
await ctx.send("This command is deprecated. Use `!roleselect post <category_name> #channel` instead.")
@commands.command(name="update_role_selectors", hidden=True)
@commands.is_owner()
async def update_role_selectors_old(self, ctx: commands.Context):
await ctx.send("This command is deprecated. Use `!roleselect post <category_name> #channel` to update an existing selector message.")
@commands.command(name="recreate_role_embeds", hidden=True)
@commands.is_owner()
async def recreate_role_embeds_old(self, ctx: commands.Context, channel: Optional[discord.TextChannel] = None):
# This command's old logic is complex and relies on EXPECTED_ROLES and CATEGORY_DETAILS.
# Re-implementing it fully with the new DB structure is out of scope for this refactor's immediate goals.
# Admins should use `roleselect removecategory` and `roleselect post` for similar functionality.
await ctx.send("This command is deprecated and its full functionality is not replicated. "
"To recreate selectors, please use `!roleselect removecategory` to remove the old one "
"(this will attempt to delete the message), then `!roleselect post` to create a new one.")
async def setup(bot):
await bot.add_cog(RoleSelectorCog(bot))
print("RoleSelectorCog loaded. Persistent views will be registered once the bot is ready.")