discordbot/cogs/role_management_cog.py
2025-06-05 16:59:09 +00:00

682 lines
32 KiB
Python

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