1162 lines
66 KiB
Python
1162 lines
66 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.")
|