567 lines
20 KiB
Python
567 lines
20 KiB
Python
"""
|
||
Admin League Management Commands
|
||
|
||
Administrative commands for manual control of league state and transaction processing.
|
||
Provides manual override capabilities for the automated freeze/thaw system.
|
||
"""
|
||
from typing import Optional
|
||
|
||
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.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 tasks.transaction_freeze import resolve_contested_transactions
|
||
|
||
|
||
class LeagueManagementCommands(commands.Cog):
|
||
"""Administrative commands for league state and transaction management."""
|
||
|
||
def __init__(self, bot: commands.Bot):
|
||
self.bot = bot
|
||
self.logger = get_contextual_logger(f'{__name__}.LeagueManagementCommands')
|
||
|
||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||
"""Check if user has admin permissions."""
|
||
if not interaction.user.guild_permissions.administrator: #type:ignore
|
||
await interaction.response.send_message(
|
||
"❌ You need administrator permissions to use admin commands.",
|
||
ephemeral=True
|
||
)
|
||
return False
|
||
return True
|
||
|
||
@app_commands.command(
|
||
name="admin-freeze-begin",
|
||
description="[ADMIN] Manually trigger freeze begin (increment week, set freeze)"
|
||
)
|
||
@league_admin_only()
|
||
@logged_command("/admin-freeze-begin")
|
||
async def admin_freeze_begin(self, interaction: discord.Interaction):
|
||
"""Manually trigger the freeze begin process."""
|
||
await interaction.response.defer()
|
||
|
||
# Get current state
|
||
current = await league_service.get_current_state()
|
||
if not current:
|
||
await interaction.followup.send(
|
||
"❌ Could not retrieve current league state.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Check if already frozen
|
||
if current.freeze:
|
||
embed = EmbedTemplate.warning(
|
||
title="Already Frozen",
|
||
description=f"League is already in freeze period for week {current.week}."
|
||
)
|
||
embed.add_field(
|
||
name="Current State",
|
||
value=f"**Week:** {current.week}\n**Freeze:** {current.freeze}\n**Season:** {current.season}",
|
||
inline=False
|
||
)
|
||
await interaction.followup.send(embed=embed)
|
||
return
|
||
|
||
# Increment week and set freeze
|
||
new_week = current.week + 1
|
||
updated = await league_service.update_current_state(
|
||
week=new_week,
|
||
freeze=True
|
||
)
|
||
|
||
if not updated:
|
||
await interaction.followup.send(
|
||
"❌ Failed to update league state.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Create success embed
|
||
embed = EmbedTemplate.success(
|
||
title="Freeze Period Begun",
|
||
description=f"Manually triggered freeze begin for week {new_week}."
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Previous State",
|
||
value=f"**Week:** {current.week}\n**Freeze:** {current.freeze}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="New State",
|
||
value=f"**Week:** {new_week}\n**Freeze:** True",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Actions Performed",
|
||
value="✅ Week incremented\n✅ Freeze flag set to True",
|
||
inline=False
|
||
)
|
||
|
||
embed.add_field(
|
||
name="⚠️ Manual Steps Required",
|
||
value="• Post freeze announcement to #transaction-log\n"
|
||
"• Post weekly info to #weekly-info (if weeks 1-18)\n"
|
||
"• Run regular transactions if needed",
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text=f"Triggered by {interaction.user.display_name}")
|
||
await interaction.followup.send(embed=embed)
|
||
|
||
@app_commands.command(
|
||
name="admin-freeze-end",
|
||
description="[ADMIN] Manually trigger freeze end (process transactions, unfreeze)"
|
||
)
|
||
@league_admin_only()
|
||
@logged_command("/admin-freeze-end")
|
||
async def admin_freeze_end(self, interaction: discord.Interaction):
|
||
"""Manually trigger the freeze end process."""
|
||
await interaction.response.defer()
|
||
|
||
# Get current state
|
||
current = await league_service.get_current_state()
|
||
if not current:
|
||
await interaction.followup.send(
|
||
"❌ Could not retrieve current league state.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Check if currently frozen
|
||
if not current.freeze:
|
||
embed = EmbedTemplate.warning(
|
||
title="Not Frozen",
|
||
description=f"League is not currently in freeze period (week {current.week})."
|
||
)
|
||
embed.add_field(
|
||
name="Current State",
|
||
value=f"**Week:** {current.week}\n**Freeze:** {current.freeze}\n**Season:** {current.season}",
|
||
inline=False
|
||
)
|
||
await interaction.followup.send(embed=embed)
|
||
return
|
||
|
||
# Process frozen transactions
|
||
processing_msg = await interaction.followup.send(
|
||
"⏳ Processing frozen transactions...",
|
||
wait=True
|
||
)
|
||
|
||
# Get frozen transactions
|
||
transactions = await transaction_service.get_frozen_transactions_by_week(
|
||
season=current.season,
|
||
week_start=current.week,
|
||
week_end=current.week + 1
|
||
)
|
||
|
||
winning_count = 0
|
||
losing_count = 0
|
||
|
||
if transactions:
|
||
# Resolve contested transactions
|
||
winning_move_ids, losing_move_ids = await resolve_contested_transactions(
|
||
transactions,
|
||
current.season
|
||
)
|
||
|
||
# Cancel losing transactions (one API call per moveid, updates all transactions in group)
|
||
for losing_move_id in losing_move_ids:
|
||
await transaction_service.cancel_transaction(losing_move_id)
|
||
losing_count += 1
|
||
|
||
# Unfreeze winning transactions (one API call per moveid, updates all transactions in group)
|
||
for winning_move_id in winning_move_ids:
|
||
await transaction_service.unfreeze_transaction(winning_move_id)
|
||
winning_count += 1
|
||
|
||
# Update processing message
|
||
await processing_msg.edit(content="⏳ Updating league state...")
|
||
|
||
# Set freeze to False
|
||
updated = await league_service.update_current_state(freeze=False)
|
||
|
||
if not updated:
|
||
await interaction.followup.send(
|
||
"❌ Failed to update league state after processing transactions.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Create success embed
|
||
embed = EmbedTemplate.success(
|
||
title="Freeze Period Ended",
|
||
description=f"Manually triggered freeze end for week {current.week}."
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Transaction Processing",
|
||
value=f"**Total Transactions:** {len(transactions) if transactions else 0}\n"
|
||
f"**Successful:** {winning_count}\n"
|
||
f"**Cancelled:** {losing_count}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="League State",
|
||
value=f"**Week:** {current.week}\n"
|
||
f"**Freeze:** False\n"
|
||
f"**Season:** {current.season}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Actions Performed",
|
||
value=f"✅ Processed {len(transactions) if transactions else 0} frozen transactions\n"
|
||
f"✅ Resolved contested players\n"
|
||
f"✅ Freeze flag set to False",
|
||
inline=False
|
||
)
|
||
|
||
if transactions:
|
||
embed.add_field(
|
||
name="⚠️ Manual Steps Required",
|
||
value="• Post thaw announcement to #transaction-log\n"
|
||
"• Notify GMs of cancelled transactions\n"
|
||
"• Post successful transactions to #transaction-log",
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text=f"Triggered by {interaction.user.display_name}")
|
||
|
||
# Edit the processing message to show final results instead of deleting and sending new
|
||
await processing_msg.edit(content=None, embed=embed)
|
||
|
||
@app_commands.command(
|
||
name="admin-set-week",
|
||
description="[ADMIN] Manually set the current league week"
|
||
)
|
||
@app_commands.describe(
|
||
week="Week number to set (1-24)"
|
||
)
|
||
@league_admin_only()
|
||
@logged_command("/admin-set-week")
|
||
async def admin_set_week(self, interaction: discord.Interaction, week: int):
|
||
"""Manually set the current league week."""
|
||
await interaction.response.defer()
|
||
|
||
# Validate week number
|
||
if week < 1 or week > 24:
|
||
await interaction.followup.send(
|
||
"❌ Week number must be between 1 and 24.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Get current state
|
||
current = await league_service.get_current_state()
|
||
if not current:
|
||
await interaction.followup.send(
|
||
"❌ Could not retrieve current league state.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Update week
|
||
updated = await league_service.update_current_state(week=week)
|
||
|
||
if not updated:
|
||
await interaction.followup.send(
|
||
"❌ Failed to update league week.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Create success embed
|
||
embed = EmbedTemplate.success(
|
||
title="League Week Updated",
|
||
description=f"Manually set league week to {week}."
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Previous Week",
|
||
value=f"**Week:** {current.week}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="New Week",
|
||
value=f"**Week:** {week}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Current State",
|
||
value=f"**Season:** {current.season}\n"
|
||
f"**Freeze:** {current.freeze}\n"
|
||
f"**Trade Deadline:** Week {current.trade_deadline}\n"
|
||
f"**Playoffs Begin:** Week {current.playoffs_begin}",
|
||
inline=False
|
||
)
|
||
|
||
embed.add_field(
|
||
name="⚠️ Warning",
|
||
value="Manual week changes bypass automated freeze/thaw processes.\n"
|
||
"Ensure you run appropriate admin commands for transaction management.",
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text=f"Changed by {interaction.user.display_name}")
|
||
await interaction.followup.send(embed=embed)
|
||
|
||
@app_commands.command(
|
||
name="admin-set-freeze",
|
||
description="[ADMIN] Manually toggle freeze status"
|
||
)
|
||
@app_commands.describe(
|
||
freeze="True to freeze transactions, False to unfreeze"
|
||
)
|
||
@app_commands.choices(freeze=[
|
||
app_commands.Choice(name="Freeze (True)", value=1),
|
||
app_commands.Choice(name="Unfreeze (False)", value=0)
|
||
])
|
||
@league_admin_only()
|
||
@logged_command("/admin-set-freeze")
|
||
async def admin_set_freeze(self, interaction: discord.Interaction, freeze: int):
|
||
"""Manually toggle the freeze status."""
|
||
await interaction.response.defer()
|
||
|
||
freeze_bool = bool(freeze)
|
||
|
||
# Get current state
|
||
current = await league_service.get_current_state()
|
||
if not current:
|
||
await interaction.followup.send(
|
||
"❌ Could not retrieve current league state.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Check if already in desired state
|
||
if current.freeze == freeze_bool:
|
||
status = "frozen" if freeze_bool else "unfrozen"
|
||
embed = EmbedTemplate.warning(
|
||
title="No Change Needed",
|
||
description=f"League is already {status}."
|
||
)
|
||
embed.add_field(
|
||
name="Current State",
|
||
value=f"**Week:** {current.week}\n**Freeze:** {current.freeze}\n**Season:** {current.season}",
|
||
inline=False
|
||
)
|
||
await interaction.followup.send(embed=embed)
|
||
return
|
||
|
||
# Update freeze status
|
||
updated = await league_service.update_current_state(freeze=freeze_bool)
|
||
|
||
if not updated:
|
||
await interaction.followup.send(
|
||
"❌ Failed to update freeze status.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Create success embed
|
||
action = "Frozen" if freeze_bool else "Unfrozen"
|
||
embed = EmbedTemplate.success(
|
||
title=f"Transactions {action}",
|
||
description=f"Manually set freeze status to {freeze_bool}."
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Previous Status",
|
||
value=f"**Freeze:** {current.freeze}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="New Status",
|
||
value=f"**Freeze:** {freeze_bool}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Current State",
|
||
value=f"**Week:** {current.week}\n**Season:** {current.season}",
|
||
inline=False
|
||
)
|
||
|
||
if freeze_bool:
|
||
embed.add_field(
|
||
name="⚠️ Note",
|
||
value="Transactions are now frozen. Use `/admin-freeze-end` to process frozen transactions.",
|
||
inline=False
|
||
)
|
||
else:
|
||
embed.add_field(
|
||
name="⚠️ Warning",
|
||
value="Manual freeze toggle bypasses transaction processing.\n"
|
||
"Ensure frozen transactions were processed before unfreezing.",
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text=f"Changed by {interaction.user.display_name}")
|
||
await interaction.followup.send(embed=embed)
|
||
|
||
@app_commands.command(
|
||
name="admin-process-freeze",
|
||
description="[ADMIN] Manually process frozen transactions without changing freeze status"
|
||
)
|
||
@app_commands.describe(
|
||
week="Week to process transactions for (defaults to current week)",
|
||
dry_run="Preview results without making changes (default: False)"
|
||
)
|
||
@league_admin_only()
|
||
@logged_command("/admin-process-freeze")
|
||
async def admin_process_transactions(
|
||
self,
|
||
interaction: discord.Interaction,
|
||
week: Optional[int] = None,
|
||
dry_run: bool = False
|
||
):
|
||
"""Manually process frozen transactions for a specific week."""
|
||
await interaction.response.defer()
|
||
|
||
# Get current state
|
||
current = await league_service.get_current_state()
|
||
if not current:
|
||
await interaction.followup.send(
|
||
"❌ Could not retrieve current league state.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Use provided week or current week
|
||
target_week = week if week is not None else current.week
|
||
|
||
# Validate week
|
||
if target_week < 1 or target_week > 24:
|
||
await interaction.followup.send(
|
||
"❌ Week number must be between 1 and 24.",
|
||
ephemeral=True
|
||
)
|
||
return
|
||
|
||
# Send processing message
|
||
mode_text = " (DRY RUN - No changes will be made)" if dry_run else ""
|
||
processing_msg = await interaction.followup.send(
|
||
f"⏳ Processing frozen transactions for week {target_week}{mode_text}...",
|
||
wait=True
|
||
)
|
||
|
||
# Get frozen transactions for the week
|
||
transactions = await transaction_service.get_frozen_transactions_by_week(
|
||
season=current.season,
|
||
week_start=target_week,
|
||
week_end=target_week + 1
|
||
)
|
||
|
||
if not transactions:
|
||
await processing_msg.edit(
|
||
content=f"ℹ️ No frozen transactions found for week {target_week}."
|
||
)
|
||
return
|
||
|
||
# Resolve contested transactions
|
||
winning_move_ids, losing_move_ids = await resolve_contested_transactions(
|
||
transactions,
|
||
current.season
|
||
)
|
||
|
||
# Process transactions (unless dry run)
|
||
if not dry_run:
|
||
# Cancel losing transactions (one API call per moveid, updates all transactions in group)
|
||
for losing_move_id in losing_move_ids:
|
||
await transaction_service.cancel_transaction(losing_move_id)
|
||
|
||
# Unfreeze winning transactions (one API call per moveid, updates all transactions in group)
|
||
for winning_move_id in winning_move_ids:
|
||
await transaction_service.unfreeze_transaction(winning_move_id)
|
||
|
||
# Create detailed results embed
|
||
if dry_run:
|
||
embed = EmbedTemplate.info(
|
||
title="Transaction Processing Preview",
|
||
description=f"Dry run results for week {target_week} (no changes made)."
|
||
)
|
||
else:
|
||
embed = EmbedTemplate.success(
|
||
title="Transactions Processed",
|
||
description=f"Successfully processed frozen transactions for week {target_week}."
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Transaction Summary",
|
||
value=f"**Total Frozen:** {len(transactions)}\n"
|
||
f"**Successful:** {len(winning_move_ids)}\n"
|
||
f"**Cancelled:** {len(losing_move_ids)}",
|
||
inline=True
|
||
)
|
||
|
||
embed.add_field(
|
||
name="Processing Details",
|
||
value=f"**Week:** {target_week}\n"
|
||
f"**Season:** {current.season}\n"
|
||
f"**Mode:** {'Dry Run' if dry_run else 'Live'}",
|
||
inline=True
|
||
)
|
||
|
||
# Show contested transactions
|
||
if losing_move_ids:
|
||
contested_info = []
|
||
for losing_move_id in losing_move_ids:
|
||
losing_moves = [t for t in transactions if t.moveid == losing_move_id]
|
||
if losing_moves:
|
||
player_name = losing_moves[0].player.name
|
||
team_abbrev = losing_moves[0].newteam.abbrev
|
||
contested_info.append(f"• {player_name} ({team_abbrev} - cancelled)")
|
||
|
||
if contested_info:
|
||
# Limit to first 10 contested transactions
|
||
display_info = contested_info[:10]
|
||
if len(contested_info) > 10:
|
||
display_info.append(f"... and {len(contested_info) - 10} more")
|
||
|
||
embed.add_field(
|
||
name="Contested Transactions",
|
||
value="\n".join(display_info),
|
||
inline=False
|
||
)
|
||
|
||
# Add warnings
|
||
if dry_run:
|
||
embed.add_field(
|
||
name="ℹ️ Dry Run Mode",
|
||
value="No transactions were modified. Run without `dry_run` parameter to apply changes.",
|
||
inline=False
|
||
)
|
||
else:
|
||
embed.add_field(
|
||
name="⚠️ Manual Steps Required",
|
||
value="• Notify GMs of cancelled transactions\n"
|
||
"• Post successful transactions to #transaction-log\n"
|
||
"• Verify all transactions processed correctly",
|
||
inline=False
|
||
)
|
||
|
||
embed.set_footer(text=f"Processed by {interaction.user.display_name}")
|
||
|
||
# Edit the processing message to show final results instead of deleting and sending new
|
||
await processing_msg.edit(content=None, embed=embed)
|
||
|
||
|
||
async def setup(bot: commands.Bot):
|
||
"""Load the league management commands cog."""
|
||
await bot.add_cog(LeagueManagementCommands(bot))
|