major-domo-v2/commands/admin/users.py
Cal Corum 7b41520054 CLAUDE: Major bot enhancements - Admin commands, player stats, standings, schedules
Major Features Added:
• Admin Management System: Complete admin command suite with user moderation, system control, and bot maintenance tools
• Enhanced Player Commands: Added batting/pitching statistics with concurrent API calls and improved embed design
• League Standings: Full standings system with division grouping, playoff picture, and wild card visualization
• Game Schedules: Comprehensive schedule system with team filtering, series organization, and proper home/away indicators

New Admin Commands (12 total):
• /admin-status, /admin-help, /admin-reload, /admin-sync, /admin-clear
• /admin-announce, /admin-maintenance
• /admin-timeout, /admin-untimeout, /admin-kick, /admin-ban, /admin-unban, /admin-userinfo

Enhanced Player Display:
• Team logo positioned beside player name using embed author
• Smart thumbnail priority: fancycard → headshot → team logo fallback
• Concurrent batting/pitching stats fetching for performance
• Rich statistics display with team colors and comprehensive metrics

New Models & Services:
• BattingStats, PitchingStats, TeamStandings, Division, Game models
• StatsService, StandingsService, ScheduleService for data management
• CustomCommand system with CRUD operations and cleanup tasks

Bot Architecture Improvements:
• Admin commands integrated into bot.py with proper loading
• Permission checks and safety guards for moderation commands
• Enhanced error handling and comprehensive audit logging
• All 227 tests passing with new functionality

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 15:32:38 -05:00

539 lines
19 KiB
Python

"""
Admin User Management Commands
User-focused administrative commands for moderation and user management.
"""
from typing import Optional, Union
import asyncio
from datetime import datetime, timedelta
import discord
from discord.ext import commands
from discord import app_commands
from utils.logging import get_contextual_logger
from utils.decorators import logged_command
from views.embeds import EmbedColors, EmbedTemplate
class UserManagementCommands(commands.Cog):
"""User management command handlers for moderation."""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.UserManagementCommands')
async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Check if user has admin permissions."""
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-timeout",
description="Timeout a user for a specified duration"
)
@app_commands.describe(
user="User to timeout",
duration="Duration in minutes (1-10080, max 7 days)",
reason="Reason for the timeout"
)
@logged_command("/admin-timeout")
async def admin_timeout(
self,
interaction: discord.Interaction,
user: discord.Member,
duration: int,
reason: Optional[str] = "No reason provided"
):
"""Timeout a user for a specified duration."""
if duration < 1 or duration > 10080: # Max 7 days in minutes
await interaction.response.send_message(
"❌ Duration must be between 1 minute and 7 days (10080 minutes).",
ephemeral=True
)
return
await interaction.response.defer()
try:
# Calculate timeout end time
timeout_until = discord.utils.utcnow() + timedelta(minutes=duration)
# Apply timeout
await user.timeout(timeout_until, reason=f"By {interaction.user}: {reason}")
embed = EmbedTemplate.create_base_embed(
title="⏰ User Timed Out",
description=f"{user.mention} has been timed out",
color=EmbedColors.WARNING
)
embed.add_field(
name="Timeout Details",
value=f"**User:** {user.display_name} ({user.mention})\n"
f"**Duration:** {duration} minutes\n"
f"**Until:** {discord.utils.format_dt(timeout_until, 'F')}\n"
f"**Reason:** {reason}",
inline=False
)
embed.add_field(
name="Action Details",
value=f"**Moderator:** {interaction.user.mention}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
embed.set_thumbnail(url=user.display_avatar.url)
await interaction.followup.send(embed=embed)
# Log the action
self.logger.info(
f"User {user} timed out by {interaction.user} for {duration} minutes. Reason: {reason}"
)
except discord.Forbidden:
await interaction.followup.send(
"❌ Missing permissions to timeout this user.",
ephemeral=True
)
except discord.HTTPException as e:
await interaction.followup.send(
f"❌ Failed to timeout user: {str(e)}",
ephemeral=True
)
@app_commands.command(
name="admin-untimeout",
description="Remove timeout from a user"
)
@app_commands.describe(
user="User to remove timeout from",
reason="Reason for removing the timeout"
)
@logged_command("/admin-untimeout")
async def admin_untimeout(
self,
interaction: discord.Interaction,
user: discord.Member,
reason: Optional[str] = "Timeout removed by admin"
):
"""Remove timeout from a user."""
await interaction.response.defer()
if not user.is_timed_out():
await interaction.followup.send(
f"{user.display_name} is not currently timed out.",
ephemeral=True
)
return
try:
await user.timeout(None, reason=f"By {interaction.user}: {reason}")
embed = EmbedTemplate.create_base_embed(
title="✅ Timeout Removed",
description=f"Timeout removed for {user.mention}",
color=EmbedColors.SUCCESS
)
embed.add_field(
name="Action Details",
value=f"**User:** {user.display_name} ({user.mention})\n"
f"**Reason:** {reason}\n"
f"**Moderator:** {interaction.user.mention}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
embed.set_thumbnail(url=user.display_avatar.url)
await interaction.followup.send(embed=embed)
self.logger.info(
f"Timeout removed from {user} by {interaction.user}. Reason: {reason}"
)
except discord.Forbidden:
await interaction.followup.send(
"❌ Missing permissions to remove timeout from this user.",
ephemeral=True
)
except discord.HTTPException as e:
await interaction.followup.send(
f"❌ Failed to remove timeout: {str(e)}",
ephemeral=True
)
@app_commands.command(
name="admin-kick",
description="Kick a user from the server"
)
@app_commands.describe(
user="User to kick",
reason="Reason for the kick"
)
@logged_command("/admin-kick")
async def admin_kick(
self,
interaction: discord.Interaction,
user: discord.Member,
reason: Optional[str] = "No reason provided"
):
"""Kick a user from the server."""
await interaction.response.defer()
# Safety check - don't kick yourself or other admins
if user == interaction.user:
await interaction.followup.send(
"❌ You cannot kick yourself.",
ephemeral=True
)
return
if user.guild_permissions.administrator:
await interaction.followup.send(
"❌ Cannot kick administrators.",
ephemeral=True
)
return
try:
# Store user info before kicking
user_name = user.display_name
user_id = user.id
user_avatar = user.display_avatar.url
await user.kick(reason=f"By {interaction.user}: {reason}")
embed = EmbedTemplate.create_base_embed(
title="👋 User Kicked",
description=f"{user_name} has been kicked from the server",
color=EmbedColors.WARNING
)
embed.add_field(
name="Kick Details",
value=f"**User:** {user_name} (ID: {user_id})\n"
f"**Reason:** {reason}\n"
f"**Moderator:** {interaction.user.mention}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
embed.set_thumbnail(url=user_avatar)
await interaction.followup.send(embed=embed)
self.logger.warning(
f"User {user_name} (ID: {user_id}) kicked by {interaction.user}. Reason: {reason}"
)
except discord.Forbidden:
await interaction.followup.send(
"❌ Missing permissions to kick this user.",
ephemeral=True
)
except discord.HTTPException as e:
await interaction.followup.send(
f"❌ Failed to kick user: {str(e)}",
ephemeral=True
)
@app_commands.command(
name="admin-ban",
description="Ban a user from the server"
)
@app_commands.describe(
user="User to ban",
reason="Reason for the ban",
delete_messages="Whether to delete user's messages (default: False)"
)
@logged_command("/admin-ban")
async def admin_ban(
self,
interaction: discord.Interaction,
user: Union[discord.Member, discord.User],
reason: Optional[str] = "No reason provided",
delete_messages: bool = False
):
"""Ban a user from the server."""
await interaction.response.defer()
# Safety checks
if isinstance(user, discord.Member):
if user == interaction.user:
await interaction.followup.send(
"❌ You cannot ban yourself.",
ephemeral=True
)
return
if user.guild_permissions.administrator:
await interaction.followup.send(
"❌ Cannot ban administrators.",
ephemeral=True
)
return
try:
# Store user info before banning
user_name = user.display_name if hasattr(user, 'display_name') else user.name
user_id = user.id
user_avatar = user.display_avatar.url
# Delete messages from last day if requested
delete_days = 1 if delete_messages else 0
await interaction.guild.ban(
user,
reason=f"By {interaction.user}: {reason}",
delete_message_days=delete_days
)
embed = EmbedTemplate.create_base_embed(
title="🔨 User Banned",
description=f"{user_name} has been banned from the server",
color=EmbedColors.ERROR
)
embed.add_field(
name="Ban Details",
value=f"**User:** {user_name} (ID: {user_id})\n"
f"**Reason:** {reason}\n"
f"**Messages Deleted:** {'Yes (1 day)' if delete_messages else 'No'}\n"
f"**Moderator:** {interaction.user.mention}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
embed.set_thumbnail(url=user_avatar)
await interaction.followup.send(embed=embed)
self.logger.warning(
f"User {user_name} (ID: {user_id}) banned by {interaction.user}. Reason: {reason}"
)
except discord.Forbidden:
await interaction.followup.send(
"❌ Missing permissions to ban this user.",
ephemeral=True
)
except discord.HTTPException as e:
await interaction.followup.send(
f"❌ Failed to ban user: {str(e)}",
ephemeral=True
)
@app_commands.command(
name="admin-unban",
description="Unban a user from the server"
)
@app_commands.describe(
user_id="User ID to unban",
reason="Reason for the unban"
)
@logged_command("/admin-unban")
async def admin_unban(
self,
interaction: discord.Interaction,
user_id: str,
reason: Optional[str] = "Ban lifted by admin"
):
"""Unban a user from the server."""
await interaction.response.defer()
try:
# Convert user_id to int
user_id_int = int(user_id)
except ValueError:
await interaction.followup.send(
"❌ Invalid user ID format.",
ephemeral=True
)
return
try:
# Get the user object
user = await self.bot.fetch_user(user_id_int)
# Check if user is actually banned
try:
ban_entry = await interaction.guild.fetch_ban(user)
ban_reason = ban_entry.reason or "No reason recorded"
except discord.NotFound:
await interaction.followup.send(
f"❌ User {user.name} is not banned.",
ephemeral=True
)
return
# Unban the user
await interaction.guild.unban(user, reason=f"By {interaction.user}: {reason}")
embed = EmbedTemplate.create_base_embed(
title="✅ User Unbanned",
description=f"{user.name} has been unbanned",
color=EmbedColors.SUCCESS
)
embed.add_field(
name="Unban Details",
value=f"**User:** {user.name} (ID: {user_id})\n"
f"**Original Ban:** {ban_reason}\n"
f"**Unban Reason:** {reason}\n"
f"**Moderator:** {interaction.user.mention}\n"
f"**Time:** {discord.utils.utcnow().strftime('%H:%M:%S UTC')}",
inline=False
)
embed.set_thumbnail(url=user.display_avatar.url)
await interaction.followup.send(embed=embed)
self.logger.info(
f"User {user.name} (ID: {user_id}) unbanned by {interaction.user}. Reason: {reason}"
)
except discord.NotFound:
await interaction.followup.send(
f"❌ Could not find user with ID {user_id}.",
ephemeral=True
)
except discord.Forbidden:
await interaction.followup.send(
"❌ Missing permissions to unban users.",
ephemeral=True
)
except discord.HTTPException as e:
await interaction.followup.send(
f"❌ Failed to unban user: {str(e)}",
ephemeral=True
)
@app_commands.command(
name="admin-userinfo",
description="Display detailed information about a user"
)
@app_commands.describe(
user="User to get information about"
)
@logged_command("/admin-userinfo")
async def admin_userinfo(
self,
interaction: discord.Interaction,
user: discord.Member
):
"""Display comprehensive user information."""
await interaction.response.defer()
embed = EmbedTemplate.create_base_embed(
title=f"👤 User Information - {user.display_name}",
color=EmbedColors.INFO
)
# Basic user info
embed.add_field(
name="Basic Information",
value=f"**Username:** {user.name}\n"
f"**Display Name:** {user.display_name}\n"
f"**User ID:** {user.id}\n"
f"**Bot:** {'Yes' if user.bot else 'No'}",
inline=True
)
# Account dates
created_at = discord.utils.format_dt(user.created_at, 'F')
joined_at = discord.utils.format_dt(user.joined_at, 'F') if user.joined_at else 'Unknown'
embed.add_field(
name="Account Dates",
value=f"**Account Created:** {created_at}\n"
f"**Joined Server:** {joined_at}",
inline=True
)
# Status and activity
status_emoji = {
discord.Status.online: "🟢",
discord.Status.idle: "🟡",
discord.Status.dnd: "🔴",
discord.Status.offline: ""
}.get(user.status, "")
activity_text = "None"
if user.activity:
if user.activity.type == discord.ActivityType.playing:
activity_text = f"Playing {user.activity.name}"
elif user.activity.type == discord.ActivityType.listening:
activity_text = f"Listening to {user.activity.name}"
elif user.activity.type == discord.ActivityType.watching:
activity_text = f"Watching {user.activity.name}"
else:
activity_text = str(user.activity)
embed.add_field(
name="Status & Activity",
value=f"**Status:** {status_emoji} {user.status.name.title()}\n"
f"**Activity:** {activity_text}",
inline=False
)
# Roles
roles = [role.mention for role in user.roles[1:]] # Skip @everyone
roles_text = ", ".join(roles[-10:]) if roles else "No roles"
if len(roles) > 10:
roles_text += f"\n... and {len(roles) - 10} more"
embed.add_field(
name="Roles",
value=roles_text,
inline=False
)
# Permissions check
perms = []
if user.guild_permissions.administrator:
perms.append("Administrator")
if user.guild_permissions.manage_guild:
perms.append("Manage Server")
if user.guild_permissions.manage_channels:
perms.append("Manage Channels")
if user.guild_permissions.manage_messages:
perms.append("Manage Messages")
if user.guild_permissions.kick_members:
perms.append("Kick Members")
if user.guild_permissions.ban_members:
perms.append("Ban Members")
embed.add_field(
name="Key Permissions",
value=", ".join(perms) if perms else "None",
inline=False
)
# Timeout status
if user.is_timed_out():
timeout_until = discord.utils.format_dt(user.timed_out_until, 'F')
embed.add_field(
name="⏰ Timeout Status",
value=f"**Timed out until:** {timeout_until}",
inline=False
)
embed.set_thumbnail(url=user.display_avatar.url)
embed.set_footer(text=f"Requested by {interaction.user.display_name}")
await interaction.followup.send(embed=embed)
async def setup(bot: commands.Bot):
"""Load the user management commands cog."""
await bot.add_cog(UserManagementCommands(bot))