- Fix int_timestamp() no-arg path returning seconds instead of
milliseconds, which would silently break the daily scout token cap
against the real API
- Acknowledge double-click interactions with ephemeral message instead
of silently returning (Discord requires all interactions to be acked)
- Reorder scout flow: create card copy before consuming token so a
failure doesn't cost the player a token for nothing
- Move build_scouted_card_list import to top of scout_view.py
- Remove unused asyncio import from helpers/scouting.py
- Fix footer text inconsistency ("One scout per player" everywhere)
- Update tests for new operation order and double-click behavior
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
191 lines
6.0 KiB
Python
191 lines
6.0 KiB
Python
"""
|
|
General Utilities
|
|
|
|
This module contains standalone utility functions with minimal dependencies,
|
|
including timestamp conversion, position abbreviations, and simple helpers.
|
|
"""
|
|
|
|
import datetime
|
|
from typing import Optional
|
|
import discord
|
|
|
|
|
|
def int_timestamp(datetime_obj: Optional[datetime.datetime] = None):
|
|
"""Convert a datetime to an integer millisecond timestamp.
|
|
|
|
If no argument is given, uses the current time.
|
|
"""
|
|
if datetime_obj is None:
|
|
datetime_obj = datetime.datetime.now()
|
|
return int(datetime.datetime.timestamp(datetime_obj) * 1000)
|
|
|
|
|
|
def midnight_timestamp() -> int:
|
|
"""Return today's midnight (00:00:00) as an integer millisecond timestamp."""
|
|
now = datetime.datetime.now()
|
|
midnight = datetime.datetime(now.year, now.month, now.day, 0, 0, 0)
|
|
return int_timestamp(midnight)
|
|
|
|
|
|
def get_pos_abbrev(field_pos: str) -> str:
|
|
"""Convert position name to standard abbreviation."""
|
|
if field_pos.lower() == "catcher":
|
|
return "C"
|
|
elif field_pos.lower() == "first baseman":
|
|
return "1B"
|
|
elif field_pos.lower() == "second baseman":
|
|
return "2B"
|
|
elif field_pos.lower() == "third baseman":
|
|
return "3B"
|
|
elif field_pos.lower() == "shortstop":
|
|
return "SS"
|
|
elif field_pos.lower() == "left fielder":
|
|
return "LF"
|
|
elif field_pos.lower() == "center fielder":
|
|
return "CF"
|
|
elif field_pos.lower() == "right fielder":
|
|
return "RF"
|
|
else:
|
|
return "P"
|
|
|
|
|
|
def position_name_to_abbrev(position_name):
|
|
"""Convert position name to abbreviation (alternate format)."""
|
|
if position_name == "Catcher":
|
|
return "C"
|
|
elif position_name == "First Base":
|
|
return "1B"
|
|
elif position_name == "Second Base":
|
|
return "2B"
|
|
elif position_name == "Third Base":
|
|
return "3B"
|
|
elif position_name == "Shortstop":
|
|
return "SS"
|
|
elif position_name == "Left Field":
|
|
return "LF"
|
|
elif position_name == "Center Field":
|
|
return "CF"
|
|
elif position_name == "Right Field":
|
|
return "RF"
|
|
elif position_name == "Pitcher":
|
|
return "P"
|
|
else:
|
|
return position_name
|
|
|
|
|
|
def user_has_role(user: discord.User | discord.Member, role_name: str) -> bool:
|
|
"""Check if a Discord user has a specific role."""
|
|
for x in user.roles:
|
|
if x.name == role_name:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_roster_sheet_legacy(team):
|
|
"""Get legacy roster sheet URL for a team."""
|
|
return f"https://docs.google.com/spreadsheets/d/{team.gsheet}/edit"
|
|
|
|
|
|
def get_roster_sheet(team):
|
|
"""
|
|
Get roster sheet URL for a team.
|
|
|
|
Handles both dict and Team object formats.
|
|
"""
|
|
# Handle both dict (team["gsheet"]) and object (team.gsheet) formats
|
|
gsheet = (
|
|
team.get("gsheet") if isinstance(team, dict) else getattr(team, "gsheet", None)
|
|
)
|
|
return f"https://docs.google.com/spreadsheets/d/{gsheet}/edit"
|
|
|
|
|
|
def get_player_url(team, player) -> str:
|
|
"""Generate player URL for SBA or Baseball Reference."""
|
|
if team.get("league") == "SBA":
|
|
return f'https://statsplus.net/super-baseball-association/player/{player["player_id"]}'
|
|
else:
|
|
return f'https://www.baseball-reference.com/players/{player["bbref_id"][0]}/{player["bbref_id"]}.shtml'
|
|
|
|
|
|
def owner_only(ctx) -> bool:
|
|
"""Check if user is the bot owner."""
|
|
# ID for discord User Cal
|
|
owners = [287463767924137994, 1087936030899347516]
|
|
|
|
# Handle both Context (has .author) and Interaction (has .user) objects
|
|
user = getattr(ctx, "user", None) or getattr(ctx, "author", None)
|
|
|
|
if user and user.id in owners:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_context_user(ctx):
|
|
"""
|
|
Get the user from either a Context or Interaction object.
|
|
|
|
Hybrid commands can receive either commands.Context (from prefix commands)
|
|
or discord.Interaction (from slash commands). This helper safely extracts
|
|
the user from either type.
|
|
|
|
Returns:
|
|
discord.User or discord.Member: The user who invoked the command
|
|
"""
|
|
# Handle both Context (has .author) and Interaction (has .user) objects
|
|
return getattr(ctx, "user", None) or getattr(ctx, "author", None)
|
|
|
|
|
|
def get_cal_user(ctx):
|
|
"""Get the Cal user from context. Always returns an object with .mention attribute."""
|
|
import logging
|
|
|
|
logger = logging.getLogger("discord_app")
|
|
|
|
# Define placeholder user class first
|
|
class PlaceholderUser:
|
|
def __init__(self):
|
|
self.mention = "<@287463767924137994>"
|
|
self.id = 287463767924137994
|
|
|
|
# Handle both Context and Interaction objects
|
|
if hasattr(ctx, "bot"): # Context object
|
|
bot = ctx.bot
|
|
logger.debug("get_cal_user: Using Context object")
|
|
elif hasattr(ctx, "client"): # Interaction object
|
|
bot = ctx.client
|
|
logger.debug("get_cal_user: Using Interaction object")
|
|
else:
|
|
logger.error("get_cal_user: No bot or client found in context")
|
|
return PlaceholderUser()
|
|
|
|
if not bot:
|
|
logger.error("get_cal_user: bot is None")
|
|
return PlaceholderUser()
|
|
|
|
logger.debug(f"get_cal_user: Searching among members")
|
|
try:
|
|
for user in bot.get_all_members():
|
|
if user.id == 287463767924137994:
|
|
logger.debug("get_cal_user: Found user in get_all_members")
|
|
return user
|
|
except Exception as e:
|
|
logger.error(f"get_cal_user: Exception in get_all_members: {e}")
|
|
|
|
# Fallback: try to get user directly by ID
|
|
logger.debug("get_cal_user: User not found in get_all_members, trying get_user")
|
|
try:
|
|
user = bot.get_user(287463767924137994)
|
|
if user:
|
|
logger.debug("get_cal_user: Found user via get_user")
|
|
return user
|
|
else:
|
|
logger.debug("get_cal_user: get_user returned None")
|
|
except Exception as e:
|
|
logger.error(f"get_cal_user: Exception in get_user: {e}")
|
|
|
|
# Last resort: return a placeholder user object with mention
|
|
logger.debug("get_cal_user: Using placeholder user")
|
|
return PlaceholderUser()
|