686 lines
26 KiB
Python
686 lines
26 KiB
Python
"""
|
|
Transaction Freeze/Thaw Task for Discord Bot v2.0
|
|
|
|
Automated weekly system for freezing and processing transactions.
|
|
Runs on a schedule to increment weeks and process contested transactions.
|
|
"""
|
|
import random
|
|
from datetime import datetime, UTC
|
|
from typing import Dict, List, Tuple, Set
|
|
from dataclasses import dataclass
|
|
|
|
import discord
|
|
from discord.ext import commands, tasks
|
|
|
|
from services.league_service import league_service
|
|
from services.transaction_service import transaction_service
|
|
from services.standings_service import standings_service
|
|
from models.current import Current
|
|
from models.transaction import Transaction
|
|
from utils.logging import get_contextual_logger
|
|
from views.embeds import EmbedTemplate, EmbedColors
|
|
from config import get_config
|
|
|
|
|
|
@dataclass
|
|
class TransactionPriority:
|
|
"""
|
|
Data class for transaction priority calculation.
|
|
Used to resolve contested transactions (multiple teams wanting same player).
|
|
"""
|
|
transaction: Transaction
|
|
team_win_percentage: float
|
|
tiebreaker: float # win% + small random number for randomized tiebreak
|
|
|
|
def __lt__(self, other):
|
|
"""Allow sorting by tiebreaker value."""
|
|
return self.tiebreaker < other.tiebreaker
|
|
|
|
|
|
async def resolve_contested_transactions(
|
|
transactions: List[Transaction],
|
|
season: int
|
|
) -> Tuple[List[str], List[str]]:
|
|
"""
|
|
Resolve contested transactions where multiple teams want the same player.
|
|
|
|
This is extracted as a pure function for testability.
|
|
|
|
Args:
|
|
transactions: List of all frozen transactions for the week
|
|
season: Current season number
|
|
|
|
Returns:
|
|
Tuple of (winning_move_ids, losing_move_ids)
|
|
"""
|
|
logger = get_contextual_logger(f'{__name__}.resolve_contested_transactions')
|
|
|
|
# Group transactions by player name
|
|
player_transactions: Dict[str, List[Transaction]] = {}
|
|
|
|
for transaction in transactions:
|
|
player_name = transaction.player.name.lower()
|
|
|
|
# Only consider transactions where a team is acquiring a player (not FA drops)
|
|
if transaction.newteam.abbrev.upper() != 'FA':
|
|
if player_name not in player_transactions:
|
|
player_transactions[player_name] = []
|
|
player_transactions[player_name].append(transaction)
|
|
|
|
# Identify contested players (multiple teams want same player)
|
|
contested_players: Dict[str, List[Transaction]] = {}
|
|
non_contested_moves: Set[str] = set()
|
|
|
|
for player_name, player_transactions_list in player_transactions.items():
|
|
if len(player_transactions_list) > 1:
|
|
contested_players[player_name] = player_transactions_list
|
|
logger.info(f"Contested player: {player_name} ({len(player_transactions_list)} teams)")
|
|
else:
|
|
# Non-contested, automatically wins
|
|
non_contested_moves.add(player_transactions_list[0].moveid)
|
|
|
|
# Resolve contests using team priority (win% + random tiebreaker)
|
|
winning_move_ids: Set[str] = set()
|
|
losing_move_ids: Set[str] = set()
|
|
|
|
for player_name, contested_transactions in contested_players.items():
|
|
priorities: List[TransactionPriority] = []
|
|
|
|
for transaction in contested_transactions:
|
|
# Get team for priority calculation
|
|
# If adding to MiL team, use the parent ML team for standings
|
|
if transaction.newteam.abbrev.endswith('MiL'):
|
|
team_abbrev = transaction.newteam.abbrev[:-3] # Remove 'MiL' suffix
|
|
else:
|
|
team_abbrev = transaction.newteam.abbrev
|
|
|
|
try:
|
|
# Get team standings to calculate win percentage
|
|
standings = await standings_service.get_team_standings(team_abbrev, season)
|
|
|
|
if standings and standings.wins is not None and standings.losses is not None:
|
|
total_games = standings.wins + standings.losses
|
|
win_pct = standings.wins / total_games if total_games > 0 else 0.0
|
|
else:
|
|
win_pct = 0.0
|
|
logger.warning(f"Could not get standings for {team_abbrev}, using 0.0 win%")
|
|
|
|
# Add small random component for tiebreaking (5 decimal precision)
|
|
random_component = random.randint(10000, 99999) * 0.00000001
|
|
tiebreaker = win_pct + random_component
|
|
|
|
priorities.append(TransactionPriority(
|
|
transaction=transaction,
|
|
team_win_percentage=win_pct,
|
|
tiebreaker=tiebreaker
|
|
))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error calculating priority for {team_abbrev}: {e}")
|
|
# Give them 0.0 priority on error
|
|
priorities.append(TransactionPriority(
|
|
transaction=transaction,
|
|
team_win_percentage=0.0,
|
|
tiebreaker=random.randint(10000, 99999) * 0.00000001
|
|
))
|
|
|
|
# Sort by tiebreaker (lowest win% wins - worst teams get priority)
|
|
priorities.sort()
|
|
|
|
# First team wins, rest lose
|
|
if priorities:
|
|
winner = priorities[0]
|
|
winning_move_ids.add(winner.transaction.moveid)
|
|
|
|
logger.info(
|
|
f"Contest resolved for {player_name}: {winner.transaction.newteam.abbrev} wins "
|
|
f"(win%: {winner.team_win_percentage:.3f}, tiebreaker: {winner.tiebreaker:.8f})"
|
|
)
|
|
|
|
for loser in priorities[1:]:
|
|
losing_move_ids.add(loser.transaction.moveid)
|
|
logger.info(
|
|
f"Contest lost for {player_name}: {loser.transaction.newteam.abbrev} "
|
|
f"(win%: {loser.team_win_percentage:.3f}, tiebreaker: {loser.tiebreaker:.8f})"
|
|
)
|
|
|
|
# Add non-contested moves to winners
|
|
winning_move_ids.update(non_contested_moves)
|
|
|
|
return list(winning_move_ids), list(losing_move_ids)
|
|
|
|
|
|
class TransactionFreezeTask:
|
|
"""Automated weekly freeze/thaw system for transactions."""
|
|
|
|
def __init__(self, bot: commands.Bot):
|
|
self.bot = bot
|
|
self.logger = get_contextual_logger(f'{__name__}.TransactionFreezeTask')
|
|
self.weekly_warning_sent = False # Prevent duplicate error notifications
|
|
self.logger.info("Transaction freeze/thaw task initialized")
|
|
|
|
# Start the weekly loop
|
|
self.weekly_loop.start()
|
|
|
|
def cog_unload(self):
|
|
"""Stop the task when cog is unloaded."""
|
|
self.weekly_loop.cancel()
|
|
|
|
@tasks.loop(minutes=1)
|
|
async def weekly_loop(self):
|
|
"""
|
|
Main loop that checks time and triggers freeze/thaw operations.
|
|
|
|
Runs every minute and checks:
|
|
- Monday 00:00 -> Begin freeze (increment week, set freeze flag)
|
|
- Saturday 00:00 -> End freeze (process frozen transactions)
|
|
"""
|
|
try:
|
|
self.logger.info("Weekly loop check starting")
|
|
config = get_config()
|
|
|
|
# Skip if offseason mode is enabled
|
|
if config.offseason_flag:
|
|
self.logger.info("Skipping freeze/thaw operations - offseason mode enabled")
|
|
return
|
|
|
|
# Get current league state
|
|
current = await league_service.get_current_state()
|
|
if not current:
|
|
self.logger.warning("Could not get current league state")
|
|
return
|
|
|
|
now = datetime.now()
|
|
self.logger.info(
|
|
f"Weekly loop check",
|
|
datetime=now.isoformat(),
|
|
weekday=now.weekday(),
|
|
hour=now.hour,
|
|
current_week=current.week,
|
|
freeze_status=current.freeze
|
|
)
|
|
|
|
# BEGIN FREEZE: Monday at 00:00, not already frozen
|
|
if now.weekday() == 0 and now.hour == 0 and not current.freeze and self.weekly_warning_sent:
|
|
self.logger.info("Triggering freeze begin")
|
|
await self._begin_freeze(current)
|
|
self.weekly_warning_sent = False
|
|
|
|
# END FREEZE: Saturday at 00:00, currently frozen
|
|
elif now.weekday() == 5 and now.hour == 0 and current.freeze and not self.weekly_warning_sent:
|
|
self.logger.info("Triggering freeze end")
|
|
await self._end_freeze(current)
|
|
self.weekly_warning_sent = True
|
|
|
|
else:
|
|
self.logger.debug("No freeze/thaw action needed at this time")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Unhandled exception in weekly_loop: {e}", exc_info=True)
|
|
error_message = (
|
|
f"⚠️ **Weekly Freeze Task Failed**\n"
|
|
f"```\n"
|
|
f"Error: {str(e)}\n"
|
|
f"Time: {datetime.now(UTC).isoformat()}\n"
|
|
f"Task: weekly_loop in transaction_freeze.py\n"
|
|
f"```"
|
|
)
|
|
|
|
try:
|
|
if not self.weekly_warning_sent:
|
|
await self._send_owner_notification(error_message)
|
|
self.weekly_warning_sent = True
|
|
except Exception as notify_error:
|
|
self.logger.error(f"Failed to send error notification: {notify_error}")
|
|
|
|
@weekly_loop.before_loop
|
|
async def before_weekly_loop(self):
|
|
"""Wait for bot to be ready before starting."""
|
|
await self.bot.wait_until_ready()
|
|
self.logger.info("Bot is ready, transaction freeze/thaw task starting")
|
|
|
|
async def _begin_freeze(self, current: Current):
|
|
"""
|
|
Begin weekly freeze period.
|
|
|
|
Actions:
|
|
1. Increment current week
|
|
2. Set freeze flag to True
|
|
3. Run regular transactions for current week
|
|
4. Send freeze announcement
|
|
5. Post weekly info (weeks 1-18 only)
|
|
"""
|
|
try:
|
|
self.logger.info(f"Beginning freeze for week {current.week}")
|
|
|
|
# Increment week and set freeze via service
|
|
new_week = current.week + 1
|
|
updated_current = await league_service.update_current_state(
|
|
week=new_week,
|
|
freeze=True
|
|
)
|
|
|
|
if not updated_current:
|
|
raise Exception("Failed to update current state during freeze begin")
|
|
|
|
self.logger.info(f"Week incremented to {new_week}, freeze set to True")
|
|
|
|
# Update local current object with returned data
|
|
current.week = updated_current.week
|
|
current.freeze = updated_current.freeze
|
|
|
|
# Run regular transactions for the new week
|
|
await self._run_transactions(current)
|
|
|
|
# Send freeze announcement
|
|
await self._send_freeze_announcement(current.week, is_beginning=True)
|
|
|
|
# Post weekly info for weeks 1-18
|
|
if 1 <= current.week <= 18:
|
|
await self._post_weekly_info(current)
|
|
|
|
self.logger.info(f"Freeze begin completed for week {current.week}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error in _begin_freeze: {e}", exc_info=True)
|
|
raise
|
|
|
|
async def _end_freeze(self, current: Current):
|
|
"""
|
|
End weekly freeze period.
|
|
|
|
Actions:
|
|
1. Process frozen transactions with priority resolution
|
|
2. Set freeze flag to False
|
|
3. Send thaw announcement
|
|
"""
|
|
try:
|
|
self.logger.info(f"Ending freeze for week {current.week}")
|
|
|
|
# Process frozen transactions BEFORE unfreezing
|
|
await self._process_frozen_transactions(current)
|
|
|
|
# Set freeze to False via service
|
|
updated_current = await league_service.update_current_state(freeze=False)
|
|
|
|
if not updated_current:
|
|
raise Exception("Failed to update current state during freeze end")
|
|
|
|
self.logger.info(f"Freeze set to False for week {current.week}")
|
|
|
|
# Update local current object
|
|
current.freeze = updated_current.freeze
|
|
|
|
# Send thaw announcement
|
|
await self._send_freeze_announcement(current.week, is_beginning=False)
|
|
|
|
self.logger.info(f"Freeze end completed for week {current.week}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error in _end_freeze: {e}", exc_info=True)
|
|
raise
|
|
|
|
async def _run_transactions(self, current: Current):
|
|
"""
|
|
Process regular (non-frozen) transactions for the current week.
|
|
|
|
These are transactions that take effect immediately.
|
|
"""
|
|
try:
|
|
# Get all non-frozen transactions for current week
|
|
client = await transaction_service.get_client()
|
|
params = [
|
|
('season', str(current.season)),
|
|
('week_start', str(current.week)),
|
|
('week_end', str(current.week))
|
|
]
|
|
|
|
response = await client.get('transactions', params=params)
|
|
|
|
if not response or response.get('count', 0) == 0:
|
|
self.logger.info(f"No regular transactions to process for week {current.week}")
|
|
return
|
|
|
|
transactions = response.get('transactions', [])
|
|
self.logger.info(f"Processing {len(transactions)} regular transactions for week {current.week}")
|
|
|
|
# Note: The actual player updates would happen via the API here
|
|
# For now, we just log them - the API handles the actual roster updates
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error running transactions: {e}", exc_info=True)
|
|
|
|
async def _process_frozen_transactions(self, current: Current):
|
|
"""
|
|
Process frozen transactions with priority resolution.
|
|
|
|
Uses the NEW transaction logic (no backup implementation).
|
|
|
|
Steps:
|
|
1. Get all frozen transactions for current week
|
|
2. Resolve contested transactions (multiple teams want same player)
|
|
3. Cancel losing transactions
|
|
4. Unfreeze and post winning transactions
|
|
"""
|
|
try:
|
|
# Get all frozen transactions for current week via service
|
|
transactions = await transaction_service.get_frozen_transactions_by_week(
|
|
season=current.season,
|
|
week_start=current.week,
|
|
week_end=current.week + 1
|
|
)
|
|
|
|
if not transactions:
|
|
self.logger.warning(f"No frozen transactions to process for week {current.week}")
|
|
return
|
|
|
|
self.logger.info(f"Processing {len(transactions)} frozen transactions for week {current.week}")
|
|
|
|
# Resolve contested transactions
|
|
winning_move_ids, losing_move_ids = await resolve_contested_transactions(
|
|
transactions,
|
|
current.season
|
|
)
|
|
|
|
# Cancel losing transactions via service
|
|
for losing_move_id in losing_move_ids:
|
|
try:
|
|
# Get all moves with this moveid (could be multiple players in one transaction)
|
|
losing_moves = [t for t in transactions if t.moveid == losing_move_id]
|
|
|
|
if losing_moves:
|
|
# Cancel the entire transaction (all moves with same moveid)
|
|
for move in losing_moves:
|
|
success = await transaction_service.cancel_transaction(move.moveid)
|
|
if not success:
|
|
self.logger.warning(f"Failed to cancel transaction {move.moveid}")
|
|
|
|
# Notify the GM(s) about cancellation
|
|
first_move = losing_moves[0]
|
|
|
|
# Determine which team to notify (the team that was trying to acquire)
|
|
team_for_notification = (first_move.newteam
|
|
if first_move.newteam.abbrev.upper() != 'FA'
|
|
else first_move.oldteam)
|
|
|
|
await self._notify_gm_of_cancellation(first_move, team_for_notification)
|
|
|
|
contested_players = [move.player.name for move in losing_moves]
|
|
self.logger.info(
|
|
f"Cancelled transaction {losing_move_id} due to contested players: "
|
|
f"{contested_players}"
|
|
)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error cancelling transaction {losing_move_id}: {e}")
|
|
|
|
# Unfreeze winning transactions and post to log via service
|
|
for winning_move_id in winning_move_ids:
|
|
try:
|
|
# Get all moves with this moveid
|
|
winning_moves = [t for t in transactions if t.moveid == winning_move_id]
|
|
|
|
for move in winning_moves:
|
|
# Unfreeze the transaction via service
|
|
success = await transaction_service.unfreeze_transaction(move.moveid)
|
|
if not success:
|
|
self.logger.warning(f"Failed to unfreeze transaction {move.moveid}")
|
|
|
|
# Post to transaction log
|
|
await self._post_transaction_to_log(winning_move_id, transactions)
|
|
|
|
self.logger.info(f"Processed successful transaction {winning_move_id}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error processing winning transaction {winning_move_id}: {e}")
|
|
|
|
self.logger.info(
|
|
f"Freeze processing complete: {len(winning_move_ids)} successful transactions, "
|
|
f"{len(losing_move_ids)} cancelled transactions"
|
|
)
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error during freeze processing: {e}", exc_info=True)
|
|
raise
|
|
|
|
async def _send_freeze_announcement(self, week: int, is_beginning: bool):
|
|
"""
|
|
Send freeze/thaw announcement to transaction log channel.
|
|
|
|
Args:
|
|
week: Current week number
|
|
is_beginning: True for freeze begin, False for freeze end
|
|
"""
|
|
try:
|
|
config = get_config()
|
|
guild = self.bot.get_guild(config.guild_id)
|
|
if not guild:
|
|
self.logger.warning("Could not find guild for freeze announcement")
|
|
return
|
|
|
|
channel = discord.utils.get(guild.text_channels, name='transaction-log')
|
|
if not channel:
|
|
self.logger.warning("Could not find transaction-log channel")
|
|
return
|
|
|
|
# Create announcement message (formatted like legacy bot)
|
|
week_num = f'Week {week}'
|
|
stars = '*' * 32
|
|
|
|
if is_beginning:
|
|
message = (
|
|
f'```\n'
|
|
f'{stars}\n'
|
|
f'{week_num:>9} Freeze Period Begins\n'
|
|
f'{stars}\n'
|
|
f'```'
|
|
)
|
|
else:
|
|
message = (
|
|
f'```\n'
|
|
f'{"*" * 30}\n'
|
|
f'{week_num:>9} Freeze Period Ends\n'
|
|
f'{"*" * 30}\n'
|
|
f'```'
|
|
)
|
|
|
|
await channel.send(message)
|
|
self.logger.info(f"Freeze announcement sent for week {week} ({'begin' if is_beginning else 'end'})")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error sending freeze announcement: {e}")
|
|
|
|
async def _post_weekly_info(self, current: Current):
|
|
"""
|
|
Post weekly schedule information to #weekly-info channel.
|
|
|
|
Args:
|
|
current: Current league state
|
|
"""
|
|
try:
|
|
config = get_config()
|
|
guild = self.bot.get_guild(config.guild_id)
|
|
if not guild:
|
|
return
|
|
|
|
info_channel = discord.utils.get(guild.text_channels, name='weekly-info')
|
|
if not info_channel:
|
|
self.logger.warning("Could not find weekly-info channel")
|
|
return
|
|
|
|
# Clear recent messages (last 25)
|
|
async for message in info_channel.history(limit=25):
|
|
try:
|
|
await message.delete()
|
|
except:
|
|
pass # Ignore deletion errors
|
|
|
|
# Determine season emoji
|
|
if current.week <= 5:
|
|
season_str = "🌼 **Spring**"
|
|
elif current.week > 14:
|
|
season_str = "🍂 **Fall**"
|
|
else:
|
|
season_str = "🏖️ **Summer**"
|
|
|
|
# Determine day/night schedule
|
|
night_str = "🌙 Night"
|
|
day_str = "🌞 Day"
|
|
is_div_week = current.week in [1, 3, 6, 14, 16, 18]
|
|
|
|
weekly_str = (
|
|
f'**Season**: {season_str}\n'
|
|
f'**Time of Day**: {night_str} / {night_str if is_div_week else day_str} / '
|
|
f'{night_str} / {day_str}'
|
|
)
|
|
|
|
# Send info messages
|
|
await info_channel.send(
|
|
content=(
|
|
f'Each team has manage permissions in their home ballpark. '
|
|
f'They may pin messages and rename the channel.\n\n'
|
|
f'**Make sure your ballpark starts with your team abbreviation.**'
|
|
)
|
|
)
|
|
await info_channel.send(weekly_str)
|
|
|
|
self.logger.info(f"Weekly info posted for week {current.week}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error posting weekly info: {e}")
|
|
|
|
async def _post_transaction_to_log(
|
|
self,
|
|
move_id: str,
|
|
all_transactions: List[Transaction]
|
|
):
|
|
"""
|
|
Post a transaction to the transaction log channel.
|
|
|
|
Args:
|
|
move_id: Transaction move ID
|
|
all_transactions: List of all transactions to find moves with this ID
|
|
"""
|
|
try:
|
|
config = get_config()
|
|
guild = self.bot.get_guild(config.guild_id)
|
|
if not guild:
|
|
return
|
|
|
|
channel = discord.utils.get(guild.text_channels, name='transaction-log')
|
|
if not channel:
|
|
return
|
|
|
|
# Get all moves with this moveid
|
|
moves = [t for t in all_transactions if t.moveid == move_id]
|
|
if not moves:
|
|
return
|
|
|
|
# Determine the team for the embed (team making the moves)
|
|
first_move = moves[0]
|
|
if first_move.newteam.abbrev.upper() != 'FA' and 'IL' not in first_move.newteam.abbrev:
|
|
this_team = first_move.newteam
|
|
elif first_move.oldteam.abbrev.upper() != 'FA' and 'IL' not in first_move.oldteam.abbrev:
|
|
this_team = first_move.oldteam
|
|
else:
|
|
# Default to newteam if both are FA/IL
|
|
this_team = first_move.newteam
|
|
|
|
# Build move string
|
|
move_string = ""
|
|
week_num = first_move.week
|
|
|
|
for move in moves:
|
|
move_string += (
|
|
f'**{move.player.name}** ({move.player.wara:.1f}) '
|
|
f'from {move.oldteam.abbrev} to {move.newteam.abbrev}\n'
|
|
)
|
|
|
|
# Create embed
|
|
embed = EmbedTemplate.create_base_embed(
|
|
title=f'Week {week_num} Transaction',
|
|
description=this_team.sname if hasattr(this_team, 'sname') else this_team.lname,
|
|
color=EmbedColors.INFO
|
|
)
|
|
|
|
# Set team color if available
|
|
if hasattr(this_team, 'color') and this_team.color:
|
|
try:
|
|
embed.color = discord.Color(int(this_team.color.replace('#', ''), 16))
|
|
except:
|
|
pass # Use default color on error
|
|
|
|
embed.add_field(name='Player Moves', value=move_string, inline=False)
|
|
|
|
await channel.send(embed=embed)
|
|
self.logger.info(f"Transaction posted to log: {move_id}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error posting transaction to log: {e}")
|
|
|
|
async def _notify_gm_of_cancellation(
|
|
self,
|
|
transaction: Transaction,
|
|
team
|
|
):
|
|
"""
|
|
Send DM to GM(s) about cancelled transaction.
|
|
|
|
Args:
|
|
transaction: The cancelled transaction
|
|
team: Team whose GMs should be notified
|
|
"""
|
|
try:
|
|
config = get_config()
|
|
guild = self.bot.get_guild(config.guild_id)
|
|
if not guild:
|
|
return
|
|
|
|
cancel_text = (
|
|
f'Your transaction for **{transaction.player.name}** has been cancelled '
|
|
f'because another team successfully claimed them during the freeze period.'
|
|
)
|
|
|
|
# Notify GM1
|
|
if hasattr(team, 'gmid') and team.gmid:
|
|
try:
|
|
gm_one = guild.get_member(team.gmid)
|
|
if gm_one:
|
|
await gm_one.send(cancel_text)
|
|
self.logger.info(f"Cancellation notification sent to GM1 of {team.abbrev}")
|
|
except Exception as e:
|
|
self.logger.error(f"Could not notify GM1 of {team.abbrev}: {e}")
|
|
|
|
# Notify GM2 if exists
|
|
if hasattr(team, 'gmid2') and team.gmid2:
|
|
try:
|
|
gm_two = guild.get_member(team.gmid2)
|
|
if gm_two:
|
|
await gm_two.send(cancel_text)
|
|
self.logger.info(f"Cancellation notification sent to GM2 of {team.abbrev}")
|
|
except Exception as e:
|
|
self.logger.error(f"Could not notify GM2 of {team.abbrev}: {e}")
|
|
|
|
except Exception as e:
|
|
self.logger.error(f"Error notifying GM of cancellation: {e}")
|
|
|
|
async def _send_owner_notification(self, message: str):
|
|
"""
|
|
Send error notification to bot owner.
|
|
|
|
Args:
|
|
message: Error message to send
|
|
"""
|
|
try:
|
|
app_info = await self.bot.application_info()
|
|
if app_info.owner:
|
|
await app_info.owner.send(message)
|
|
self.logger.info("Owner notification sent")
|
|
except Exception as e:
|
|
self.logger.error(f"Could not send owner notification: {e}")
|
|
|
|
|
|
def setup_freeze_task(bot: commands.Bot) -> TransactionFreezeTask:
|
|
"""Set up the transaction freeze/thaw task."""
|
|
return TransactionFreezeTask(bot)
|