major-domo-v2/commands/admin/management.py
Cal Corum 1d6fef51ab Consolidate season config variables to single source (v2.21.0)
Remove redundant sba_current_season and pd_current_season config values.
All code now uses sba_season and pd_season, which properly read from
environment variables. Fixes /team command defaulting to Season 12.

- Remove duplicate *_current_season constants from config.py
- Update 100+ references across commands, services, and utils
- sba_season defaults to 13, pd_season defaults to 10
- Environment variables SBA_SEASON/PD_SEASON now work correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 16:12:16 -06:00

898 lines
34 KiB
Python

"""
Admin Management Commands
Administrative commands for league management and bot maintenance.
"""
import asyncio
from typing import List, Dict, Any
import discord
from discord.ext import commands
from discord import app_commands
from config import get_config
from utils.logging import get_contextual_logger
from utils.decorators import logged_command
from utils.discord_helpers import set_channel_visibility
from utils.permissions import league_admin_only
from views.embeds import EmbedColors, EmbedTemplate
from services.league_service import league_service
from services.transaction_service import transaction_service
from services.player_service import player_service
class AdminCommands(commands.Cog):
"""Administrative command handlers for league management."""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.AdminCommands')
async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Check if user has admin permissions."""
# Check if interaction is from a guild and user is a Member
if not isinstance(interaction.user, discord.Member):
await interaction.response.send_message(
"❌ Admin commands can only be used in a server.",
ephemeral=True
)
return False
if not interaction.user.guild_permissions.administrator:
await interaction.response.send_message(
"❌ You need administrator permissions to use admin commands.",
ephemeral=True
)
return False
return True
@app_commands.command(
name="admin-status",
description="Display bot status and system information"
)
@league_admin_only()
@logged_command("/admin-status")
async def admin_status(self, interaction: discord.Interaction):
"""Display comprehensive bot status information."""
await interaction.response.defer()
# Gather system information
guilds_count = len(self.bot.guilds)
users_count = sum(guild.member_count or 0 for guild in self.bot.guilds)
commands_count = len([cmd for cmd in self.bot.tree.walk_commands()])
# Bot uptime calculation
uptime = discord.utils.utcnow() - self.bot.user.created_at if self.bot.user else None
uptime_str = f"{uptime.days} days" if uptime else "Unknown"
embed = EmbedTemplate.create_base_embed(
title="🤖 Bot Status - Admin Panel",
color=EmbedColors.INFO
)
# System Stats
embed.add_field(
name="System Information",
value=f"**Guilds:** {guilds_count}\n"
f"**Users:** {users_count:,}\n"
f"**Commands:** {commands_count}\n"
f"**Uptime:** {uptime_str}",
inline=True
)
# Bot Information
embed.add_field(
name="Bot Information",
value=f"**Latency:** {round(self.bot.latency * 1000)}ms\n"
f"**Version:** Discord.py {discord.__version__}\n"
f"**Current Season:** {get_config().sba_season}",
inline=True
)
# Cog Status
loaded_cogs = list(self.bot.cogs.keys())
embed.add_field(
name="Loaded Cogs",
value="\n".join([f"{cog}" for cog in loaded_cogs[:10]]) +
(f"\n... and {len(loaded_cogs) - 10} more" if len(loaded_cogs) > 10 else ""),
inline=False
)
embed.set_footer(text="Admin Status • Use /admin-help for more commands")
await interaction.followup.send(embed=embed)
@app_commands.command(
name="admin-help",
description="Display available admin commands and their usage"
)
@league_admin_only()
@logged_command("/admin-help")
async def admin_help(self, interaction: discord.Interaction):
"""Display comprehensive admin help information."""
await interaction.response.defer()
embed = EmbedTemplate.create_base_embed(
title="🛠️ Admin Commands - Help",
description="Available administrative commands for league management",
color=EmbedColors.PRIMARY
)
# System Commands
embed.add_field(
name="System Management",
value="**`/admin-status`** - Display bot status and information\n"
"**`/admin-reload <cog>`** - Reload a specific cog\n"
"**`/admin-sync`** - Sync application commands\n"
"**`/admin-clear <count>`** - Clear messages from channel\n"
"**`/admin-clear-scorecards`** - Clear live scorebug channel and hide it",
inline=False
)
# League Management
embed.add_field(
name="League Management",
value="**`/admin-season <season>`** - Set current season\n"
"**`/admin-announce <message>`** - Send announcement to channel\n"
"**`/admin-maintenance <on/off>`** - Toggle maintenance mode\n"
"**`/admin-process-transactions [week]`** - Manually process weekly transactions",
inline=False
)
# User Management
embed.add_field(
name="User Management",
value="**`/admin-timeout <user> <duration>`** - Timeout a user\n"
"**`/admin-kick <user> <reason>`** - Kick a user\n"
"**`/admin-ban <user> <reason>`** - Ban a user",
inline=False
)
embed.add_field(
name="Usage Notes",
value="• All admin commands require Administrator permissions\n"
"• Commands are logged for audit purposes\n"
"• Use with caution - some actions are irreversible",
inline=False
)
embed.set_footer(text="Administrator Permissions Required")
await interaction.followup.send(embed=embed)
@app_commands.command(
name="admin-reload",
description="Reload a specific bot cog"
)
@app_commands.describe(
cog="Name of the cog to reload (e.g., 'commands.players.info')"
)
@league_admin_only()
@logged_command("/admin-reload")
async def admin_reload(self, interaction: discord.Interaction, cog: str):
"""Reload a specific cog for hot-swapping code changes."""
await interaction.response.defer()
try:
# Attempt to reload the cog
await self.bot.reload_extension(cog)
embed = EmbedTemplate.create_base_embed(
title="✅ Cog Reloaded Successfully",
description=f"Successfully reloaded `{cog}`",
color=EmbedColors.SUCCESS
)
embed.add_field(
name="Reload Details",
value=f"**Cog:** {cog}\n"
f"**Status:** Successfully reloaded\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
except commands.ExtensionNotFound:
embed = EmbedTemplate.create_base_embed(
title="❌ Cog Not Found",
description=f"Could not find cog: `{cog}`",
color=EmbedColors.ERROR
)
except commands.ExtensionNotLoaded:
embed = EmbedTemplate.create_base_embed(
title="❌ Cog Not Loaded",
description=f"Cog `{cog}` is not currently loaded",
color=EmbedColors.ERROR
)
except Exception as e:
embed = EmbedTemplate.create_base_embed(
title="❌ Reload Failed",
description=f"Failed to reload `{cog}`: {str(e)}",
color=EmbedColors.ERROR
)
await interaction.followup.send(embed=embed)
@app_commands.command(
name="admin-sync",
description="Sync application commands with Discord"
)
@app_commands.describe(
local="Sync to this guild only (fast, for development)",
clear_local="Clear locally synced commands (does not sync after clearing)"
)
@league_admin_only()
@logged_command("/admin-sync")
async def admin_sync(
self,
interaction: discord.Interaction,
local: bool = False,
clear_local: bool = False
):
"""Sync slash commands with Discord API."""
await interaction.response.defer()
try:
# Clear local commands if requested
if clear_local:
if not interaction.guild_id:
raise ValueError("Cannot clear local commands outside of a guild")
self.logger.info(f"Clearing local commands for guild {interaction.guild_id}")
self.bot.tree.clear_commands(guild=discord.Object(id=interaction.guild_id))
embed = EmbedTemplate.create_base_embed(
title="✅ Local Commands Cleared",
description=f"Cleared all commands synced to this guild",
color=EmbedColors.SUCCESS
)
embed.add_field(
name="Clear Details",
value=f"**Guild ID:** {interaction.guild_id}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}\n"
f"**Note:** Commands not synced after clearing",
inline=False
)
await interaction.followup.send(embed=embed)
return
# Determine sync target
if local:
if not interaction.guild_id:
raise ValueError("Cannot sync locally outside of a guild")
guild = discord.Object(id=interaction.guild_id)
sync_type = "local guild"
else:
guild = None
sync_type = "globally"
# Perform sync
self.logger.info(f"Syncing commands {sync_type}")
synced_commands = await self.bot.tree.sync(guild=guild)
embed = EmbedTemplate.create_base_embed(
title="✅ Commands Synced Successfully",
description=f"Synced {len(synced_commands)} application commands {sync_type}",
color=EmbedColors.SUCCESS
)
# Show some of the synced commands
command_names = [cmd.name for cmd in synced_commands[:10]]
embed.add_field(
name="Synced Commands",
value="\n".join([f"• /{name}" for name in command_names]) +
(f"\n... and {len(synced_commands) - 10} more" if len(synced_commands) > 10 else ""),
inline=False
)
embed.add_field(
name="Sync Details",
value=f"**Total Commands:** {len(synced_commands)}\n"
f"**Sync Type:** {sync_type.title()}\n"
f"**Guild ID:** {interaction.guild_id or 'N/A'}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
except Exception as e:
self.logger.error(f"Sync failed: {e}", exc_info=True)
embed = EmbedTemplate.create_base_embed(
title="❌ Sync Failed",
description=f"Failed to sync commands: {str(e)}",
color=EmbedColors.ERROR
)
await interaction.followup.send(embed=embed)
@commands.command(name="admin-sync")
@league_admin_only()
async def admin_sync_prefix(self, ctx: commands.Context):
"""
Prefix command version of admin-sync for bootstrap scenarios.
Use this when slash commands aren't synced yet and you can't access /admin-sync.
Syncs to the current guild only (for multi-bot scenarios).
"""
self.logger.info(f"Prefix command !admin-sync invoked by {ctx.author} in {ctx.guild}")
try:
# Sync to current guild only (not globally) for multi-bot scenarios
synced_commands = await self.bot.tree.sync(guild=ctx.guild)
embed = EmbedTemplate.create_base_embed(
title="✅ Commands Synced Successfully",
description=f"Synced {len(synced_commands)} application commands",
color=EmbedColors.SUCCESS
)
# Show some of the synced commands
command_names = [cmd.name for cmd in synced_commands[:10]]
embed.add_field(
name="Synced Commands",
value="\n".join([f"• /{name}" for name in command_names]) +
(f"\n... and {len(synced_commands) - 10} more" if len(synced_commands) > 10 else ""),
inline=False
)
embed.add_field(
name="Sync Details",
value=f"**Total Commands:** {len(synced_commands)}\n"
f"**Sync Type:** Local Guild\n"
f"**Guild ID:** {ctx.guild.id}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
embed.set_footer(text="💡 Use /admin-sync local:True for guild-only sync")
except Exception as e:
self.logger.error(f"Prefix command sync failed: {e}", exc_info=True)
embed = EmbedTemplate.create_base_embed(
title="❌ Sync Failed",
description=f"Failed to sync commands: {str(e)}",
color=EmbedColors.ERROR
)
await ctx.send(embed=embed)
@app_commands.command(
name="admin-clear",
description="Clear messages from the current channel"
)
@app_commands.describe(
count="Number of messages to delete (1-100)"
)
@league_admin_only()
@logged_command("/admin-clear")
async def admin_clear(self, interaction: discord.Interaction, count: int):
"""Clear a specified number of messages from the channel."""
if count < 1 or count > 100:
await interaction.response.send_message(
"❌ Count must be between 1 and 100.",
ephemeral=True
)
return
await interaction.response.defer()
# Verify channel type supports purge
if not isinstance(interaction.channel, (discord.TextChannel, discord.Thread, discord.VoiceChannel, discord.StageChannel)):
await interaction.followup.send(
"❌ Cannot purge messages in this channel type.",
ephemeral=True
)
return
try:
deleted = await interaction.channel.purge(limit=count)
embed = EmbedTemplate.create_base_embed(
title="🗑️ Messages Cleared",
description=f"Successfully deleted {len(deleted)} messages",
color=EmbedColors.SUCCESS
)
embed.add_field(
name="Clear Details",
value=f"**Messages Deleted:** {len(deleted)}\n"
f"**Channel:** {interaction.channel.mention}\n"
f"**Requested:** {count} messages\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
# Send confirmation and auto-delete after 5 seconds
message = await interaction.followup.send(embed=embed)
await asyncio.sleep(5)
if message:
try:
await message.delete()
except discord.NotFound:
pass # Message already deleted
except discord.Forbidden:
await interaction.followup.send(
"❌ Missing permissions to delete messages.",
ephemeral=True
)
except Exception as e:
await interaction.followup.send(
f"❌ Failed to clear messages: {str(e)}",
ephemeral=True
)
@app_commands.command(
name="admin-announce",
description="Send an announcement to the current channel"
)
@app_commands.describe(
message="Announcement message to send",
mention_everyone="Whether to mention @everyone (default: False)"
)
@league_admin_only()
@logged_command("/admin-announce")
async def admin_announce(
self,
interaction: discord.Interaction,
message: str,
mention_everyone: bool = False
):
"""Send an official announcement to the channel."""
await interaction.response.defer()
embed = EmbedTemplate.create_base_embed(
title="📢 League Announcement",
description=message,
color=EmbedColors.PRIMARY
)
embed.set_footer(
text=f"Announcement by {interaction.user.display_name}",
icon_url=interaction.user.display_avatar.url
)
# Send with or without mention based on flag
if mention_everyone:
await interaction.followup.send(content="@everyone", embed=embed)
else:
await interaction.followup.send(embed=embed)
# Log the announcement
self.logger.info(
f"Announcement sent by {interaction.user} in {interaction.channel}: {message[:100]}..."
)
@app_commands.command(
name="admin-maintenance",
description="Toggle maintenance mode for the bot"
)
@app_commands.describe(
mode="Turn maintenance mode on or off"
)
@app_commands.choices(mode=[
app_commands.Choice(name="On", value="on"),
app_commands.Choice(name="Off", value="off")
])
@league_admin_only()
@logged_command("/admin-maintenance")
async def admin_maintenance(self, interaction: discord.Interaction, mode: str):
"""Toggle maintenance mode to prevent normal command usage."""
await interaction.response.defer()
# This would typically set a global flag or database value
# For now, we'll just show the interface
is_enabling = mode.lower() == "on"
status_text = "enabled" if is_enabling else "disabled"
color = EmbedColors.WARNING if is_enabling else EmbedColors.SUCCESS
embed = EmbedTemplate.create_base_embed(
title=f"🔧 Maintenance Mode {status_text.title()}",
description=f"Maintenance mode has been **{status_text}**",
color=color
)
if is_enabling:
embed.add_field(
name="Maintenance Active",
value="• Normal commands are disabled\n"
"• Only admin commands are available\n"
"• Users will see maintenance message",
inline=False
)
else:
embed.add_field(
name="Maintenance Ended",
value="• All commands are now available\n"
"• Normal bot operation resumed\n"
"• Users can access all features",
inline=False
)
embed.add_field(
name="Status Change",
value=f"**Changed by:** {interaction.user.mention}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}\n"
f"**Mode:** {status_text.title()}",
inline=False
)
await interaction.followup.send(embed=embed)
@app_commands.command(
name="admin-clear-scorecards",
description="Manually clear the live scorebug channel and hide it from members"
)
@league_admin_only()
@logged_command("/admin-clear-scorecards")
async def admin_clear_scorecards(self, interaction: discord.Interaction):
"""
Manually clear #live-sba-scores channel and set @everyone view permission to off.
This is useful for:
- Cleaning up stale scorebug displays
- Manually hiding the channel when games finish
- Testing channel visibility functionality
"""
await interaction.response.defer()
# Get the live-sba-scores channel
config = get_config()
guild = self.bot.get_guild(config.guild_id)
if not guild:
await interaction.followup.send(
"❌ Could not find guild. Check configuration.",
ephemeral=True
)
return
live_scores_channel = discord.utils.get(guild.text_channels, name='live-sba-scores')
if not live_scores_channel:
await interaction.followup.send(
"❌ Could not find #live-sba-scores channel.",
ephemeral=True
)
return
try:
# Clear all messages from the channel
deleted_count = 0
async for message in live_scores_channel.history(limit=100):
try:
await message.delete()
deleted_count += 1
except discord.NotFound:
pass # Message already deleted
self.logger.info(f"Cleared {deleted_count} messages from #live-sba-scores")
# Hide channel from @everyone
visibility_success = await set_channel_visibility(
live_scores_channel,
visible=False,
reason="Admin manual clear via /admin-clear-scorecards"
)
if visibility_success:
visibility_status = "✅ Hidden from @everyone"
else:
visibility_status = "⚠️ Could not change visibility (check permissions)"
# Create success embed
embed = EmbedTemplate.success(
title="Live Scorebug Channel Cleared",
description="Successfully cleared the #live-sba-scores channel"
)
embed.add_field(
name="Clear Details",
value=f"**Channel:** {live_scores_channel.mention}\n"
f"**Messages Deleted:** {deleted_count}\n"
f"**Visibility:** {visibility_status}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
embed.add_field(
name="Next Steps",
value="• Channel is now hidden from @everyone\n"
"• Bot retains full access to the channel\n"
"• Channel will auto-show when games are published\n"
"• Live scorebug tracker runs every 3 minutes",
inline=False
)
await interaction.followup.send(embed=embed)
except discord.Forbidden:
await interaction.followup.send(
"❌ Missing permissions to clear messages or modify channel permissions.",
ephemeral=True
)
except Exception as e:
self.logger.error(f"Error clearing scorecards: {e}", exc_info=True)
await interaction.followup.send(
f"❌ Failed to clear channel: {str(e)}",
ephemeral=True
)
@app_commands.command(
name="admin-process-transactions",
description="[ADMIN] Manually process all transactions for the current week (or specified week)"
)
@app_commands.describe(
week="Week number to process (optional, defaults to current week)"
)
@league_admin_only()
@logged_command("/admin-process-transactions")
async def admin_process_transactions(
self,
interaction: discord.Interaction,
week: int | None = None
):
"""
Manually process all transactions for the current week.
This is a fallback mechanism if the Monday morning task fails to run.
It will:
1. Get all non-frozen, non-cancelled transactions for the specified week
2. Execute each transaction by updating player rosters via the API
3. Report success/failure counts
Args:
week: Optional week number to process. If not provided, uses current week.
"""
await interaction.response.defer()
try:
# Get current league state
current = await league_service.get_current_state()
if not current:
await interaction.followup.send(
"❌ Could not get current league state from the API.",
ephemeral=True
)
return
# Use provided week or current week
target_week = week if week is not None else current.week
target_season = current.season
self.logger.info(
f"Processing transactions for week {target_week}, season {target_season}",
requested_by=str(interaction.user)
)
# Get all non-frozen, non-cancelled transactions for the target week using service layer
transactions = await transaction_service.get_all_items(params=[
('season', str(target_season)),
('week_start', str(target_week)),
('week_end', str(target_week)),
('frozen', 'false'),
('cancelled', 'false')
])
if not transactions:
embed = EmbedTemplate.info(
title="No Transactions to Process",
description=f"No non-frozen, non-cancelled transactions found for Week {target_week}"
)
embed.add_field(
name="Search Criteria",
value=f"**Season:** {target_season}\n"
f"**Week:** {target_week}\n"
f"**Frozen:** No\n"
f"**Cancelled:** No",
inline=False
)
await interaction.followup.send(embed=embed)
return
# Count total transactions
total_count = len(transactions)
self.logger.info(f"Found {total_count} transactions to process for week {target_week}")
# Process each transaction
success_count = 0
failure_count = 0
errors: List[Dict[str, Any]] = []
# Create initial status embed
processing_embed = EmbedTemplate.loading(
title="Processing Transactions",
description=f"Processing {total_count} transactions for Week {target_week}..."
)
processing_embed.add_field(
name="Progress",
value="Starting...",
inline=False
)
status_message = await interaction.followup.send(embed=processing_embed)
for idx, transaction in enumerate(transactions, start=1):
try:
# Execute player roster update via service layer
await self._execute_player_update(
player_id=transaction.player.id,
new_team_id=transaction.newteam.id,
player_name=transaction.player.name
)
success_count += 1
# Update progress every 5 transactions or on last transaction
if idx % 5 == 0 or idx == total_count:
processing_embed.set_field_at(
0,
name="Progress",
value=f"Processed {idx}/{total_count} transactions\n"
f"✅ Successful: {success_count}\n"
f"❌ Failed: {failure_count}",
inline=False
)
await status_message.edit(embed=processing_embed) # type: ignore
# Rate limiting: 100ms delay between requests
await asyncio.sleep(0.1)
except Exception as e:
failure_count += 1
error_info = {
'player': transaction.player.name,
'player_id': transaction.player.id,
'new_team': transaction.newteam.abbrev,
'error': str(e)
}
errors.append(error_info)
self.logger.error(
f"Failed to execute transaction for {error_info['player']}",
player_id=error_info['player_id'],
new_team=error_info['new_team'],
error=e
)
# Create completion embed
if failure_count == 0:
completion_embed = EmbedTemplate.success(
title="Transactions Processed Successfully",
description=f"All {total_count} transactions for Week {target_week} have been processed."
)
elif success_count == 0:
completion_embed = EmbedTemplate.error(
title="Transaction Processing Failed",
description=f"Failed to process all {total_count} transactions for Week {target_week}."
)
else:
completion_embed = EmbedTemplate.warning(
title="Transactions Partially Processed",
description=f"Some transactions for Week {target_week} failed to process."
)
completion_embed.add_field(
name="Processing Summary",
value=f"**Total Transactions:** {total_count}\n"
f"**✅ Successful:** {success_count}\n"
f"**❌ Failed:** {failure_count}\n"
f"**Week:** {target_week}\n"
f"**Season:** {target_season}",
inline=False
)
# Add error details if there were failures
if errors:
error_text = ""
for error in errors[:5]: # Show first 5 errors
error_text += f"• **{error['player']}** → {error['new_team']}: {error['error'][:50]}\n"
if len(errors) > 5:
error_text += f"\n... and {len(errors) - 5} more errors"
completion_embed.add_field(
name="Errors",
value=error_text,
inline=False
)
completion_embed.add_field(
name="Next Steps",
value="• Verify transactions in the database\n"
"• Check #transaction-log channel for posted moves\n"
"• Review any errors and retry if necessary",
inline=False
)
completion_embed.set_footer(
text=f"Processed by {interaction.user.display_name}{discord.utils.utcnow().strftime('%H:%M:%S UTC')}"
)
# Update the status message with final results
await status_message.edit(embed=completion_embed) # type: ignore
self.logger.info(
f"Transaction processing complete for week {target_week}",
success=success_count,
failures=failure_count,
total=total_count
)
except Exception as e:
self.logger.error(f"Error processing transactions: {e}", exc_info=True)
embed = EmbedTemplate.error(
title="Transaction Processing Failed",
description=f"An error occurred while processing transactions: {str(e)}"
)
await interaction.followup.send(embed=embed, ephemeral=True)
async def _execute_player_update(
self,
player_id: int,
new_team_id: int,
player_name: str
) -> bool:
"""
Execute a player roster update via service layer.
Args:
player_id: Player database ID
new_team_id: New team ID to assign
player_name: Player name for logging
Returns:
True if update successful, False otherwise
Raises:
Exception: If API call fails
"""
try:
self.logger.info(
f"Updating player roster",
player_id=player_id,
player_name=player_name,
new_team_id=new_team_id
)
# Execute player team update via service layer
updated_player = await player_service.update_player_team(
player_id=player_id,
new_team_id=new_team_id
)
# Verify update was successful
if updated_player:
self.logger.info(
f"Successfully updated player roster",
player_id=player_id,
player_name=player_name,
new_team_id=new_team_id
)
return True
else:
self.logger.warning(
f"Player update returned no response",
player_id=player_id,
player_name=player_name,
new_team_id=new_team_id
)
return False
except Exception as e:
self.logger.error(
f"Failed to update player roster",
player_id=player_id,
player_name=player_name,
new_team_id=new_team_id,
error=e,
exc_info=True
)
raise
async def setup(bot: commands.Bot):
"""Load the admin commands cog."""
await bot.add_cog(AdminCommands(bot))