feat: Enhance role management with auto-creation of roles from presets and owner checks

This commit is contained in:
Slipstream 2025-05-30 18:25:45 -06:00
parent f1364be0e5
commit 05433c0bb8
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD

View File

@ -16,6 +16,10 @@ from api_service.api_models import (
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
@ -398,13 +402,50 @@ class RoleSelectorCog(commands.Cog):
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:
role_in_guild = interaction.guild.get_role(int(preset_role_option.role_id))
if not role_in_guild:
# Using followup for potentially multiple messages
await interaction.followup.send(f"Warning: Role '{preset_role_option.name}' (ID: {preset_role_option.role_id}) from preset not found in this server. Skipping.", ephemeral=True)
continue
roles_to_add.append(GuildRole(role_id=str(role_in_guild.id), name=role_in_guild.name, emoji=preset_role_option.emoji))
# 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
@ -483,7 +524,7 @@ class RoleSelectorCog(commands.Cog):
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.")
@app_commands.checks.is_owner() # Presets are global, so owner only
@app_commands.check(is_owner_check) # Presets are global, so owner only
async def roleselect_listpresets(self, interaction: discord.Interaction):
presets = db.get_all_role_category_presets()
if not presets:
@ -641,7 +682,7 @@ class RoleSelectorCog(commands.Cog):
# Role Preset Commands (Owner Only)
@rolepreset_group.command(name="add", description="Creates a new global role category preset.")
@app_commands.checks.is_owner()
@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.",
@ -661,7 +702,7 @@ class RoleSelectorCog(commands.Cog):
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.checks.is_owner()
@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):
@ -671,7 +712,7 @@ class RoleSelectorCog(commands.Cog):
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.checks.is_owner()
@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.",
@ -717,7 +758,7 @@ class RoleSelectorCog(commands.Cog):
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.checks.is_owner()
@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."
@ -748,11 +789,105 @@ class RoleSelectorCog(commands.Cog):
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="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)
await bot.tree.sync() # Sync slash commands
print("RoleSelectorCog loaded. Persistent views will be registered once the bot is ready. Slash commands synced.")
# 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.")