import discord from discord.ext import commands from discord import app_commands import logging from typing import Optional, List # Set up logging logger = logging.getLogger(__name__) class RoleManagementCog(commands.Cog): """Cog for comprehensive role management""" def __init__(self, bot): self.bot = bot # Create the main command group for this cog self.role_group = app_commands.Group( name="role", description="Manage server roles" ) # Register commands self.register_commands() # Add command group to the bot's tree self.bot.tree.add_command(self.role_group) def register_commands(self): """Register all commands for this cog""" # --- Create Role Command --- create_command = app_commands.Command( name="create", description="Create a new role", callback=self.role_create_callback, parent=self.role_group, ) app_commands.describe( name="The name of the new role", color="The color of the role in hex format (e.g., #FF0000 for red)", mentionable="Whether the role can be mentioned by everyone", hoist="Whether the role should be displayed separately in the member list", reason="The reason for creating this role", )(create_command) self.role_group.add_command(create_command) # --- Edit Role Command --- edit_command = app_commands.Command( name="edit", description="Edit an existing role", callback=self.role_edit_callback, parent=self.role_group, ) app_commands.describe( role="The role to edit", name="New name for the role", color="New color for the role in hex format (e.g., #FF0000 for red)", mentionable="Whether the role can be mentioned by everyone", hoist="Whether the role should be displayed separately in the member list", reason="The reason for editing this role", )(edit_command) self.role_group.add_command(edit_command) # --- Delete Role Command --- delete_command = app_commands.Command( name="delete", description="Delete a role", callback=self.role_delete_callback, parent=self.role_group, ) app_commands.describe( role="The role to delete", reason="The reason for deleting this role" )(delete_command) self.role_group.add_command(delete_command) # --- Add Role Command --- add_command = app_commands.Command( name="add", description="Add a role to a user", callback=self.role_add_callback, parent=self.role_group, ) app_commands.describe( member="The member to add the role to", role="The role to add", reason="The reason for adding this role", )(add_command) self.role_group.add_command(add_command) # --- Remove Role Command --- remove_command = app_commands.Command( name="remove", description="Remove a role from a user", callback=self.role_remove_callback, parent=self.role_group, ) app_commands.describe( member="The member to remove the role from", role="The role to remove", reason="The reason for removing this role", )(remove_command) self.role_group.add_command(remove_command) # --- List Roles Command --- list_command = app_commands.Command( name="list", description="List all roles in the server", callback=self.role_list_callback, parent=self.role_group, ) self.role_group.add_command(list_command) # --- Role Info Command --- info_command = app_commands.Command( name="info", description="View detailed information about a role", callback=self.role_info_callback, parent=self.role_group, ) app_commands.describe(role="The role to view information about")(info_command) self.role_group.add_command(info_command) # --- Change Role Position Command --- position_command = app_commands.Command( name="position", description="Change a role's position in the hierarchy", callback=self.role_position_callback, parent=self.role_group, ) app_commands.describe( role="The role to move", position="The new position for the role (1 is the lowest, excluding @everyone)", reason="The reason for changing this role's position", )(position_command) self.role_group.add_command(position_command) # --- Command Callbacks --- async def role_create_callback( self, interaction: discord.Interaction, name: str, color: Optional[str] = None, mentionable: Optional[bool] = False, hoist: Optional[bool] = False, reason: Optional[str] = None, ): """Callback for /role create command""" # Check permissions if not interaction.guild: await interaction.response.send_message( "This command can only be used in a server.", ephemeral=True ) return if not interaction.user.guild_permissions.manage_roles: await interaction.response.send_message( "You don't have permission to manage roles.", ephemeral=True ) return if not interaction.guild.me.guild_permissions.manage_roles: await interaction.response.send_message( "I don't have permission to manage roles.", ephemeral=True ) return # Parse color if provided role_color = discord.Color.default() if color: try: # Remove # if present if color.startswith("#"): color = color[1:] # Convert hex to int role_color = discord.Color(int(color, 16)) except ValueError: await interaction.response.send_message( f"Invalid color format. Please use hex format (e.g., #FF0000 for red).", ephemeral=True, ) return try: # Create the role new_role = await interaction.guild.create_role( name=name, color=role_color, hoist=hoist, mentionable=mentionable, reason=f"{reason or 'No reason provided'} (Created by {interaction.user})", ) # Create an embed with role information embed = discord.Embed( title="✅ Role Created", description=f"Successfully created role {new_role.mention}", color=role_color, ) embed.add_field(name="Name", value=name, inline=True) embed.add_field(name="Color", value=str(role_color), inline=True) embed.add_field(name="Hoisted", value="Yes" if hoist else "No", inline=True) embed.add_field( name="Mentionable", value="Yes" if mentionable else "No", inline=True ) embed.add_field( name="Created by", value=interaction.user.mention, inline=True ) if reason: embed.add_field(name="Reason", value=reason, inline=False) await interaction.response.send_message(embed=embed) logger.info( f"Role '{name}' created by {interaction.user} in {interaction.guild.name}" ) except discord.Forbidden: await interaction.response.send_message( "I don't have permission to create roles.", ephemeral=True ) except discord.HTTPException as e: await interaction.response.send_message( f"Failed to create role: {e}", ephemeral=True ) async def role_edit_callback( self, interaction: discord.Interaction, role: discord.Role, name: Optional[str] = None, color: Optional[str] = None, mentionable: Optional[bool] = None, hoist: Optional[bool] = None, reason: Optional[str] = None, ): """Callback for /role edit command""" # Check permissions if not interaction.guild: await interaction.response.send_message( "This command can only be used in a server.", ephemeral=True ) return if not interaction.user.guild_permissions.manage_roles: await interaction.response.send_message( "You don't have permission to manage roles.", ephemeral=True ) return if not interaction.guild.me.guild_permissions.manage_roles: await interaction.response.send_message( "I don't have permission to manage roles.", ephemeral=True ) return # Check if the role is manageable if not role.is_assignable() or role.is_default(): await interaction.response.send_message( "I cannot edit this role. It might be the @everyone role or higher than my highest role.", ephemeral=True, ) return # Parse color if provided role_color = None if color: try: # Remove # if present if color.startswith("#"): color = color[1:] # Convert hex to int role_color = discord.Color(int(color, 16)) except ValueError: await interaction.response.send_message( f"Invalid color format. Please use hex format (e.g., #FF0000 for red).", ephemeral=True, ) return # Store original values for the embed original_name = role.name original_color = role.color original_mentionable = role.mentionable original_hoist = role.hoist try: # Edit the role await role.edit( name=name if name is not None else role.name, color=role_color if role_color is not None else role.color, hoist=hoist if hoist is not None else role.hoist, mentionable=( mentionable if mentionable is not None else role.mentionable ), reason=f"{reason or 'No reason provided'} (Edited by {interaction.user})", ) # Create an embed with role information embed = discord.Embed( title="✅ Role Edited", description=f"Successfully edited role {role.mention}", color=role.color, ) # Only show fields that were changed if name is not None and name != original_name: embed.add_field( name="Name", value=f"{original_name} → {name}", inline=True ) if role_color is not None and role_color != original_color: embed.add_field( name="Color", value=f"{original_color} → {role.color}", inline=True ) if hoist is not None and hoist != original_hoist: embed.add_field( name="Hoisted", value=f"{'Yes' if original_hoist else 'No'} → {'Yes' if hoist else 'No'}", inline=True, ) if mentionable is not None and mentionable != original_mentionable: embed.add_field( name="Mentionable", value=f"{'Yes' if original_mentionable else 'No'} → {'Yes' if mentionable else 'No'}", inline=True, ) embed.add_field( name="Edited by", value=interaction.user.mention, inline=True ) if reason: embed.add_field(name="Reason", value=reason, inline=False) await interaction.response.send_message(embed=embed) logger.info( f"Role '{role.name}' edited by {interaction.user} in {interaction.guild.name}" ) except discord.Forbidden: await interaction.response.send_message( "I don't have permission to edit this role.", ephemeral=True ) except discord.HTTPException as e: await interaction.response.send_message( f"Failed to edit role: {e}", ephemeral=True ) async def role_delete_callback( self, interaction: discord.Interaction, role: discord.Role, reason: Optional[str] = None, ): """Callback for /role delete command""" # Check permissions if not interaction.guild: await interaction.response.send_message( "This command can only be used in a server.", ephemeral=True ) return if not interaction.user.guild_permissions.manage_roles: await interaction.response.send_message( "You don't have permission to manage roles.", ephemeral=True ) return if not interaction.guild.me.guild_permissions.manage_roles: await interaction.response.send_message( "I don't have permission to manage roles.", ephemeral=True ) return # Check if the role is manageable if not role.is_assignable() or role.is_default(): await interaction.response.send_message( "I cannot delete this role. It might be the @everyone role or higher than my highest role.", ephemeral=True, ) return # Store role info for the confirmation message role_name = role.name role_color = role.color role_members_count = len(role.members) # Confirmation message embed = discord.Embed( title="⚠️ Confirm Role Deletion", description=f"Are you sure you want to delete the role **{role_name}**?", color=role_color, ) embed.add_field(name="Role", value=role.mention, inline=True) embed.add_field( name="Members with this role", value=str(role_members_count), inline=True ) if reason: embed.add_field(name="Reason", value=reason, inline=False) # Create confirmation buttons class ConfirmView(discord.ui.View): def __init__(self): super().__init__(timeout=60) self.value = None @discord.ui.button(label="Confirm", style=discord.ButtonStyle.danger) async def confirm( self, button_interaction: discord.Interaction, button: discord.ui.Button ): if button_interaction.user.id != interaction.user.id: await button_interaction.response.send_message( "You cannot use this button.", ephemeral=True ) return self.value = True self.stop() try: # Delete the role await role.delete( reason=f"{reason or 'No reason provided'} (Deleted by {interaction.user})" ) # Create a success embed success_embed = discord.Embed( title="✅ Role Deleted", description=f"Successfully deleted role **{role_name}**", color=discord.Color.green(), ) success_embed.add_field( name="Deleted by", value=interaction.user.mention, inline=True ) if reason: success_embed.add_field( name="Reason", value=reason, inline=False ) await button_interaction.response.edit_message( embed=success_embed, view=None ) logger.info( f"Role '{role_name}' deleted by {interaction.user} in {interaction.guild.name}" ) except discord.Forbidden: await button_interaction.response.edit_message( embed=discord.Embed( title="❌ Error", description="I don't have permission to delete this role.", color=discord.Color.red(), ), view=None, ) except discord.HTTPException as e: await button_interaction.response.edit_message( embed=discord.Embed( title="❌ Error", description=f"Failed to delete role: {e}", color=discord.Color.red(), ), view=None, ) @discord.ui.button(label="Cancel", style=discord.ButtonStyle.secondary) async def cancel( self, button_interaction: discord.Interaction, button: discord.ui.Button ): if button_interaction.user.id != interaction.user.id: await button_interaction.response.send_message( "You cannot use this button.", ephemeral=True ) return self.value = False self.stop() # Create a cancellation embed cancel_embed = discord.Embed( title="❌ Cancelled", description="Role deletion cancelled.", color=discord.Color.red(), ) await button_interaction.response.edit_message( embed=cancel_embed, view=None ) # Send the confirmation message view = ConfirmView() await interaction.response.send_message(embed=embed, view=view) async def role_add_callback( self, interaction: discord.Interaction, member: discord.Member, role: discord.Role, reason: Optional[str] = None, ): """Callback for /role add command""" # Check permissions if not interaction.guild: await interaction.response.send_message( "This command can only be used in a server.", ephemeral=True ) return if not interaction.user.guild_permissions.manage_roles: await interaction.response.send_message( "You don't have permission to manage roles.", ephemeral=True ) return if not interaction.guild.me.guild_permissions.manage_roles: await interaction.response.send_message( "I don't have permission to manage roles.", ephemeral=True ) return # Check if the role is assignable if not role.is_assignable() or role.is_default(): await interaction.response.send_message( "I cannot assign this role. It might be the @everyone role or higher than my highest role.", ephemeral=True, ) return # Check if the member already has the role if role in member.roles: await interaction.response.send_message( f"{member.mention} already has the role {role.mention}.", ephemeral=True ) return try: # Add the role to the member await member.add_roles( role, reason=f"{reason or 'No reason provided'} (Added by {interaction.user})", ) # Create an embed with role information embed = discord.Embed( title="✅ Role Added", description=f"Successfully added role {role.mention} to {member.mention}", color=role.color, ) embed.add_field(name="Member", value=member.mention, inline=True) embed.add_field(name="Role", value=role.mention, inline=True) embed.add_field( name="Added by", value=interaction.user.mention, inline=True ) if reason: embed.add_field(name="Reason", value=reason, inline=False) await interaction.response.send_message(embed=embed) logger.info( f"Role '{role.name}' added to {member} by {interaction.user} in {interaction.guild.name}" ) # Attempt to DM the user try: role_info = f"{role.name} (ID: {role.id})" dm_embed = discord.Embed( title="Role Added", description=f"The role {role_info} was added to you in **{interaction.guild.name}**.", color=role.color, ) dm_embed.add_field( name="Added by", value=interaction.user.mention, inline=True ) if reason: dm_embed.add_field(name="Reason", value=reason, inline=False) await member.send(embed=dm_embed) logger.info( f"Successfully DMed {member} about role '{role.name}' addition." ) except discord.Forbidden: logger.warning( f"Failed to DM {member} about role '{role.name}' addition (Forbidden)." ) except discord.HTTPException as e: logger.warning( f"Failed to DM {member} about role '{role.name}' addition (HTTPException: {e})." ) except discord.Forbidden: await interaction.response.send_message( "I don't have permission to add roles to this member.", ephemeral=True ) except discord.HTTPException as e: await interaction.response.send_message( f"Failed to add role: {e}", ephemeral=True ) async def role_remove_callback( self, interaction: discord.Interaction, member: discord.Member, role: discord.Role, reason: Optional[str] = None, ): """Callback for /role remove command""" # Check permissions if not interaction.guild: await interaction.response.send_message( "This command can only be used in a server.", ephemeral=True ) return if not interaction.user.guild_permissions.manage_roles: await interaction.response.send_message( "You don't have permission to manage roles.", ephemeral=True ) return if not interaction.guild.me.guild_permissions.manage_roles: await interaction.response.send_message( "I don't have permission to manage roles.", ephemeral=True ) return # Check if the role is assignable if not role.is_assignable() or role.is_default(): await interaction.response.send_message( "I cannot remove this role. It might be the @everyone role or higher than my highest role.", ephemeral=True, ) return # Check if the member has the role if role not in member.roles: await interaction.response.send_message( f"{member.mention} doesn't have the role {role.mention}.", ephemeral=True, ) return try: # Remove the role from the member await member.remove_roles( role, reason=f"{reason or 'No reason provided'} (Removed by {interaction.user})", ) # Create an embed with role information embed = discord.Embed( title="✅ Role Removed", description=f"Successfully removed role {role.mention} from {member.mention}", color=role.color, ) embed.add_field(name="Member", value=member.mention, inline=True) embed.add_field(name="Role", value=role.mention, inline=True) embed.add_field( name="Removed by", value=interaction.user.mention, inline=True ) if reason: embed.add_field(name="Reason", value=reason, inline=False) await interaction.response.send_message(embed=embed) logger.info( f"Role '{role.name}' removed from {member} by {interaction.user} in {interaction.guild.name}" ) # Attempt to DM the user try: role_info = f"{role.name} (ID: {role.id})" dm_embed = discord.Embed( title="Role Removed", description=f"The role {role_info} was removed from you in **{interaction.guild.name}**.", color=role.color, ) dm_embed.add_field( name="Removed by", value=interaction.user.mention, inline=True ) if reason: dm_embed.add_field(name="Reason", value=reason, inline=False) await member.send(embed=dm_embed) logger.info( f"Successfully DMed {member} about role '{role.name}' removal." ) except discord.Forbidden: logger.warning( f"Failed to DM {member} about role '{role.name}' removal (Forbidden)." ) except discord.HTTPException as e: logger.warning( f"Failed to DM {member} about role '{role.name}' removal (HTTPException: {e})." ) except discord.Forbidden: await interaction.response.send_message( "I don't have permission to remove roles from this member.", ephemeral=True, ) except discord.HTTPException as e: await interaction.response.send_message( f"Failed to remove role: {e}", ephemeral=True ) async def role_list_callback(self, interaction: discord.Interaction): """Callback for /role list command""" # Check if in a guild if not interaction.guild: await interaction.response.send_message( "This command can only be used in a server.", ephemeral=True ) return # Get all roles in the guild, sorted by position (highest first) roles = sorted(interaction.guild.roles, key=lambda r: r.position, reverse=True) # Create an embed with role information embed = discord.Embed( title=f"Roles in {interaction.guild.name}", description=f"Total roles: {len(roles) - 1}", # Subtract 1 to exclude @everyone color=discord.Color.blue(), ) # Add roles to the embed in chunks to avoid hitting the field limit chunk_size = 20 for i in range(0, len(roles), chunk_size): chunk = roles[i : i + chunk_size] # Format the roles role_list = [] for role in chunk: if role.is_default(): # Skip @everyone continue role_list.append(f"{role.mention} - {len(role.members)} members") if role_list: embed.add_field( name=f"Roles {i+1}-{min(i+chunk_size, len(roles) - 1)}", # Subtract 1 to account for @everyone value="\n".join(role_list), inline=False, ) await interaction.response.send_message(embed=embed) async def role_info_callback( self, interaction: discord.Interaction, role: discord.Role ): """Callback for /role info command""" # Check if in a guild if not interaction.guild: await interaction.response.send_message( "This command can only be used in a server.", ephemeral=True ) return # Create an embed with detailed role information embed = discord.Embed(title=f"Role Information: {role.name}", color=role.color) # Add basic information embed.add_field(name="ID", value=role.id, inline=True) embed.add_field(name="Color", value=str(role.color), inline=True) embed.add_field(name="Position", value=role.position, inline=True) embed.add_field( name="Hoisted", value="Yes" if role.hoist else "No", inline=True ) embed.add_field( name="Mentionable", value="Yes" if role.mentionable else "No", inline=True ) embed.add_field( name="Bot Role", value="Yes" if role.is_bot_managed() else "No", inline=True ) embed.add_field( name="Integration Role", value="Yes" if role.is_integration() else "No", inline=True, ) embed.add_field(name="Members", value=len(role.members), inline=True) embed.add_field( name="Created At", value=discord.utils.format_dt(role.created_at), inline=True, ) # Add permissions information permissions = [] for perm, value in role.permissions: if value: formatted_perm = perm.replace("_", " ").title() permissions.append(f"✅ {formatted_perm}") if permissions: # Split permissions into chunks to avoid hitting the field value limit chunk_size = 10 for i in range(0, len(permissions), chunk_size): chunk = permissions[i : i + chunk_size] embed.add_field( name=( "Permissions" if i == 0 else "\u200b" ), # Use zero-width space for additional fields value="\n".join(chunk), inline=False, ) else: embed.add_field(name="Permissions", value="No permissions", inline=False) await interaction.response.send_message(embed=embed) async def role_position_callback( self, interaction: discord.Interaction, role: discord.Role, position: int, reason: Optional[str] = None, ): """Callback for /role position command""" # Check permissions if not interaction.guild: await interaction.response.send_message( "This command can only be used in a server.", ephemeral=True ) return if not interaction.user.guild_permissions.manage_roles: await interaction.response.send_message( "You don't have permission to manage roles.", ephemeral=True ) return if not interaction.guild.me.guild_permissions.manage_roles: await interaction.response.send_message( "I don't have permission to manage roles.", ephemeral=True ) return # Check if the role is manageable if not role.is_assignable() or role.is_default(): await interaction.response.send_message( "I cannot move this role. It might be the @everyone role or higher than my highest role.", ephemeral=True, ) return # Validate position if position < 1: await interaction.response.send_message( "Position must be at least 1.", ephemeral=True ) return # Get the maximum valid position (excluding @everyone) max_position = len(interaction.guild.roles) - 1 if position > max_position: await interaction.response.send_message( f"Position must be at most {max_position}.", ephemeral=True ) return # Store original position for the embed original_position = role.position try: # Convert the 1-based user-friendly position to the 0-based position used by Discord # Also account for the fact that positions are ordered from bottom to top actual_position = position # Move the role await role.edit( position=actual_position, reason=f"{reason or 'No reason provided'} (Position changed by {interaction.user})", ) # Create an embed with role information embed = discord.Embed( title="✅ Role Position Changed", description=f"Successfully changed position of role {role.mention}", color=role.color, ) embed.add_field(name="Role", value=role.mention, inline=True) embed.add_field( name="Old Position", value=str(original_position), inline=True ) embed.add_field(name="New Position", value=str(role.position), inline=True) embed.add_field( name="Changed by", value=interaction.user.mention, inline=True ) if reason: embed.add_field(name="Reason", value=reason, inline=False) await interaction.response.send_message(embed=embed) logger.info( f"Role '{role.name}' position changed from {original_position} to {role.position} by {interaction.user} in {interaction.guild.name}" ) except discord.Forbidden: await interaction.response.send_message( "I don't have permission to change this role's position.", ephemeral=True, ) except discord.HTTPException as e: await interaction.response.send_message( f"Failed to change role position: {e}", ephemeral=True ) async def setup(bot): await bot.add_cog(RoleManagementCog(bot)) logger.info("RoleManagementCog loaded successfully.")