All checks were successful
Build Docker Image / build (pull_request) Successful in 3m17s
Check-In Player packs (auto-opened by daily check-in) could end up orphaned
in inventory if roll_for_cards failed. The open-packs command crashed because:
1. The hyphenated pack type name bypassed the pretty_name logic, producing an
empty select menu that Discord rejected (400 Bad Request)
2. Even if displayed, selecting it would raise KeyError in the callback since
"Check-In Player".split("-") doesn't match any known pack type token
Fixes:
- Filter auto-open pack types out of the manual open-packs menu
- Add fallback for hyphenated pack type names in pretty_name logic
- Replace KeyError with graceful user-facing message for unknown pack types
- Change "contact an admin" to "contact Cal" in all user-facing messages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
599 lines
21 KiB
Python
599 lines
21 KiB
Python
"""
|
|
Discord Select UI components.
|
|
|
|
Contains all Select classes for various team, cardset, and pack selections.
|
|
"""
|
|
|
|
import logging
|
|
import discord
|
|
from typing import Literal, Optional
|
|
from helpers.constants import ALL_MLB_TEAMS, IMAGES, normalize_franchise
|
|
|
|
logger = logging.getLogger("discord_app")
|
|
|
|
# Team name to ID mappings
|
|
AL_TEAM_IDS = {
|
|
"Baltimore Orioles": 3,
|
|
"Boston Red Sox": 4,
|
|
"Chicago White Sox": 6,
|
|
"Cleveland Guardians": 8,
|
|
"Detroit Tigers": 10,
|
|
"Houston Astros": 11,
|
|
"Kansas City Royals": 12,
|
|
"Los Angeles Angels": 13,
|
|
"Minnesota Twins": 17,
|
|
"New York Yankees": 19,
|
|
"Oakland Athletics": 20,
|
|
"Athletics": 20, # Alias for post-Oakland move
|
|
"Seattle Mariners": 24,
|
|
"Tampa Bay Rays": 27,
|
|
"Texas Rangers": 28,
|
|
"Toronto Blue Jays": 29,
|
|
}
|
|
|
|
NL_TEAM_IDS = {
|
|
"Arizona Diamondbacks": 1,
|
|
"Atlanta Braves": 2,
|
|
"Chicago Cubs": 5,
|
|
"Cincinnati Reds": 7,
|
|
"Colorado Rockies": 9,
|
|
"Los Angeles Dodgers": 14,
|
|
"Miami Marlins": 15,
|
|
"Milwaukee Brewers": 16,
|
|
"New York Mets": 18,
|
|
"Philadelphia Phillies": 21,
|
|
"Pittsburgh Pirates": 22,
|
|
"San Diego Padres": 23,
|
|
"San Francisco Giants": 25,
|
|
"St Louis Cardinals": 26, # Note: constants has 'St Louis Cardinals' not 'St. Louis Cardinals'
|
|
"Washington Nationals": 30,
|
|
}
|
|
|
|
# Get AL teams from constants
|
|
AL_TEAMS = [team for team in ALL_MLB_TEAMS.keys() if team in AL_TEAM_IDS]
|
|
NL_TEAMS = [
|
|
team
|
|
for team in ALL_MLB_TEAMS.keys()
|
|
if team in NL_TEAM_IDS or team == "St Louis Cardinals"
|
|
]
|
|
|
|
# Cardset mappings
|
|
CARDSET_LABELS_TO_IDS = {
|
|
"2022 Season": 3,
|
|
"2022 Promos": 4,
|
|
"2021 Season": 1,
|
|
"2019 Season": 5,
|
|
"2013 Season": 6,
|
|
"2012 Season": 7,
|
|
"Mario Super Sluggers": 8,
|
|
"2023 Season": 9,
|
|
"2016 Season": 11,
|
|
"2008 Season": 12,
|
|
"2018 Season": 13,
|
|
"2024 Season": 17,
|
|
"2024 Promos": 18,
|
|
"1998 Season": 20,
|
|
"2025 Season": 24,
|
|
"2005 Live": 27,
|
|
"Pokemon - Brilliant Stars": 23,
|
|
}
|
|
|
|
|
|
def _get_team_id(team_name: str, league: Literal["AL", "NL"]) -> int:
|
|
"""Get team ID from team name and league."""
|
|
if league == "AL":
|
|
return AL_TEAM_IDS.get(team_name)
|
|
else:
|
|
# Handle the St. Louis Cardinals special case
|
|
if team_name == "St. Louis Cardinals":
|
|
return NL_TEAM_IDS.get("St Louis Cardinals")
|
|
return NL_TEAM_IDS.get(team_name)
|
|
|
|
|
|
class SelectChoicePackTeam(discord.ui.Select):
|
|
def __init__(
|
|
self, which: Literal["AL", "NL"], team, cardset_id: Optional[int] = None
|
|
):
|
|
self.which = which
|
|
self.owner_team = team
|
|
self.cardset_id = cardset_id
|
|
|
|
if which == "AL":
|
|
options = [discord.SelectOption(label=team) for team in AL_TEAMS]
|
|
else:
|
|
# Handle St. Louis Cardinals display name
|
|
options = [
|
|
discord.SelectOption(
|
|
label="St. Louis Cardinals"
|
|
if team == "St Louis Cardinals"
|
|
else team
|
|
)
|
|
for team in NL_TEAMS
|
|
]
|
|
|
|
super().__init__(placeholder=f"Select an {which} team", options=options)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
# Import here to avoid circular imports
|
|
from api_calls import db_get, db_patch
|
|
from helpers import open_choice_pack
|
|
|
|
team_id = _get_team_id(self.values[0], self.which)
|
|
if team_id is None:
|
|
raise ValueError(f"Unknown team: {self.values[0]}")
|
|
|
|
await interaction.response.edit_message(
|
|
content=f"You selected the **{self.values[0]}**", view=None
|
|
)
|
|
# Get the selected packs
|
|
params = [
|
|
("pack_type_id", 8),
|
|
("team_id", self.owner_team["id"]),
|
|
("opened", False),
|
|
("limit", 1),
|
|
("exact_match", True),
|
|
]
|
|
if self.cardset_id is not None:
|
|
params.append(("pack_cardset_id", self.cardset_id))
|
|
p_query = await db_get("packs", params=params)
|
|
if p_query["count"] == 0:
|
|
logger.error(f"open-packs - no packs found with params: {params}")
|
|
raise ValueError("Unable to open packs")
|
|
|
|
this_pack = await db_patch(
|
|
"packs",
|
|
object_id=p_query["packs"][0]["id"],
|
|
params=[("pack_team_id", team_id)],
|
|
)
|
|
|
|
await open_choice_pack(this_pack, self.owner_team, interaction, self.cardset_id)
|
|
|
|
|
|
class SelectOpenPack(discord.ui.Select):
|
|
def __init__(self, options: list, team: dict):
|
|
self.owner_team = team
|
|
super().__init__(placeholder="Select a Pack Type", options=options)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
# Import here to avoid circular imports
|
|
from api_calls import db_get
|
|
from helpers import open_st_pr_packs, open_choice_pack
|
|
|
|
logger.info(f"SelectPackChoice - selection: {self.values[0]}")
|
|
pack_vals = self.values[0].split("-")
|
|
logger.info(f"pack_vals: {pack_vals}")
|
|
|
|
# Get the selected packs
|
|
params = [
|
|
("team_id", self.owner_team["id"]),
|
|
("opened", False),
|
|
("limit", 5),
|
|
("exact_match", True),
|
|
]
|
|
|
|
open_type = "standard"
|
|
if "Standard" in pack_vals:
|
|
open_type = "standard"
|
|
params.append(("pack_type_id", 1))
|
|
elif "Premium" in pack_vals:
|
|
open_type = "standard"
|
|
params.append(("pack_type_id", 3))
|
|
elif "Daily" in pack_vals:
|
|
params.append(("pack_type_id", 4))
|
|
elif "Promo Choice" in pack_vals:
|
|
open_type = "choice"
|
|
params.append(("pack_type_id", 9))
|
|
elif "MVP" in pack_vals:
|
|
open_type = "choice"
|
|
params.append(("pack_type_id", 5))
|
|
elif "All Star" in pack_vals:
|
|
open_type = "choice"
|
|
params.append(("pack_type_id", 6))
|
|
elif "Mario" in pack_vals:
|
|
open_type = "choice"
|
|
params.append(("pack_type_id", 7))
|
|
elif "Team Choice" in pack_vals:
|
|
open_type = "choice"
|
|
params.append(("pack_type_id", 8))
|
|
else:
|
|
logger.error(
|
|
f"Unrecognized pack type in selector: {self.values[0]} (split: {pack_vals})"
|
|
)
|
|
await interaction.response.edit_message(view=None)
|
|
await interaction.followup.send(
|
|
content="This pack type cannot be opened manually. Please contact Cal.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
# If team isn't already set on team choice pack, make team pack selection now
|
|
await interaction.response.edit_message(view=None)
|
|
|
|
cardset_id = None
|
|
# Handle Team Choice packs with no team/cardset assigned
|
|
if (
|
|
"Team Choice" in pack_vals
|
|
and "Team" not in pack_vals
|
|
and "Cardset" not in pack_vals
|
|
):
|
|
await interaction.followup.send(
|
|
content="This Team Choice pack needs to be assigned a team and cardset. "
|
|
"Please contact Cal to configure this pack.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
elif "Team Choice" in pack_vals and "Cardset" in pack_vals:
|
|
# cardset_id = pack_vals[2]
|
|
cardset_index = pack_vals.index("Cardset")
|
|
cardset_id = pack_vals[cardset_index + 1]
|
|
params.append(("pack_cardset_id", cardset_id))
|
|
if "Team" not in pack_vals:
|
|
view = SelectView(
|
|
[
|
|
SelectChoicePackTeam("AL", self.owner_team, cardset_id),
|
|
SelectChoicePackTeam("NL", self.owner_team, cardset_id),
|
|
],
|
|
timeout=30,
|
|
)
|
|
await interaction.followup.send(
|
|
content="Please select a team for your Team Choice pack:", view=view
|
|
)
|
|
return
|
|
|
|
params.append(("pack_team_id", pack_vals[pack_vals.index("Team") + 1]))
|
|
else:
|
|
if "Team" in pack_vals:
|
|
params.append(("pack_team_id", pack_vals[pack_vals.index("Team") + 1]))
|
|
if "Cardset" in pack_vals:
|
|
cardset_id = pack_vals[pack_vals.index("Cardset") + 1]
|
|
params.append(("pack_cardset_id", cardset_id))
|
|
|
|
p_query = await db_get("packs", params=params)
|
|
if p_query["count"] == 0:
|
|
logger.error(f"open-packs - no packs found with params: {params}")
|
|
await interaction.followup.send(
|
|
content="Unable to find the selected pack. Please contact Cal.",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
# Open the packs
|
|
try:
|
|
if open_type == "standard":
|
|
await open_st_pr_packs(p_query["packs"], self.owner_team, interaction)
|
|
elif open_type == "choice":
|
|
await open_choice_pack(
|
|
p_query["packs"][0], self.owner_team, interaction, cardset_id
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to open pack: {e}")
|
|
await interaction.followup.send(
|
|
content=f"Failed to open pack. Please contact Cal. Error: {str(e)}",
|
|
ephemeral=True,
|
|
)
|
|
return
|
|
|
|
|
|
class SelectPaperdexCardset(discord.ui.Select):
|
|
def __init__(self):
|
|
options = [
|
|
discord.SelectOption(label="2005 Live"),
|
|
discord.SelectOption(label="2025 Season"),
|
|
discord.SelectOption(label="1998 Season"),
|
|
discord.SelectOption(label="2024 Season"),
|
|
discord.SelectOption(label="2023 Season"),
|
|
discord.SelectOption(label="2022 Season"),
|
|
discord.SelectOption(label="2022 Promos"),
|
|
discord.SelectOption(label="2021 Season"),
|
|
discord.SelectOption(label="2019 Season"),
|
|
discord.SelectOption(label="2018 Season"),
|
|
discord.SelectOption(label="2016 Season"),
|
|
discord.SelectOption(label="2013 Season"),
|
|
discord.SelectOption(label="2012 Season"),
|
|
discord.SelectOption(label="2008 Season"),
|
|
discord.SelectOption(label="Mario Super Sluggers"),
|
|
]
|
|
super().__init__(placeholder="Select a Cardset", options=options)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
# Import here to avoid circular imports
|
|
from api_calls import db_get
|
|
from helpers import get_team_by_owner, paperdex_cardset_embed, embed_pagination
|
|
|
|
logger.info(f"SelectPaperdexCardset - selection: {self.values[0]}")
|
|
cardset_id = CARDSET_LABELS_TO_IDS.get(self.values[0])
|
|
if cardset_id is None:
|
|
raise ValueError(f"Unknown cardset: {self.values[0]}")
|
|
|
|
c_query = await db_get("cardsets", object_id=cardset_id, none_okay=False)
|
|
await interaction.response.edit_message(
|
|
content="Okay, sifting through your cards...", view=None
|
|
)
|
|
|
|
cardset_embeds = await paperdex_cardset_embed(
|
|
team=await get_team_by_owner(interaction.user.id), this_cardset=c_query
|
|
)
|
|
await embed_pagination(cardset_embeds, interaction.channel, interaction.user)
|
|
|
|
|
|
class SelectPaperdexTeam(discord.ui.Select):
|
|
def __init__(self, which: Literal["AL", "NL"]):
|
|
self.which = which
|
|
|
|
if which == "AL":
|
|
options = [discord.SelectOption(label=team) for team in AL_TEAMS]
|
|
else:
|
|
# Handle St. Louis Cardinals display name
|
|
options = [
|
|
discord.SelectOption(
|
|
label="St. Louis Cardinals"
|
|
if team == "St Louis Cardinals"
|
|
else team
|
|
)
|
|
for team in NL_TEAMS
|
|
]
|
|
|
|
super().__init__(placeholder=f"Select an {which} team", options=options)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
# Import here to avoid circular imports
|
|
from api_calls import db_get
|
|
from helpers import get_team_by_owner, paperdex_team_embed, embed_pagination
|
|
|
|
team_id = _get_team_id(self.values[0], self.which)
|
|
if team_id is None:
|
|
raise ValueError(f"Unknown team: {self.values[0]}")
|
|
|
|
t_query = await db_get("teams", object_id=team_id, none_okay=False)
|
|
await interaction.response.edit_message(
|
|
content="Okay, sifting through your cards...", view=None
|
|
)
|
|
|
|
team_embeds = await paperdex_team_embed(
|
|
team=await get_team_by_owner(interaction.user.id), mlb_team=t_query
|
|
)
|
|
await embed_pagination(team_embeds, interaction.channel, interaction.user)
|
|
|
|
|
|
class SelectBuyPacksCardset(discord.ui.Select):
|
|
def __init__(
|
|
self,
|
|
team: dict,
|
|
quantity: int,
|
|
pack_type_id: int,
|
|
pack_embed: discord.Embed,
|
|
cost: int,
|
|
):
|
|
options = [
|
|
discord.SelectOption(label="2005 Live"),
|
|
discord.SelectOption(label="2025 Season"),
|
|
discord.SelectOption(label="1998 Season"),
|
|
discord.SelectOption(label="Pokemon - Brilliant Stars"),
|
|
discord.SelectOption(label="2024 Season"),
|
|
discord.SelectOption(label="2023 Season"),
|
|
discord.SelectOption(label="2022 Season"),
|
|
discord.SelectOption(label="2021 Season"),
|
|
discord.SelectOption(label="2019 Season"),
|
|
discord.SelectOption(label="2018 Season"),
|
|
discord.SelectOption(label="2016 Season"),
|
|
discord.SelectOption(label="2013 Season"),
|
|
discord.SelectOption(label="2012 Season"),
|
|
discord.SelectOption(label="2008 Season"),
|
|
]
|
|
self.team = team
|
|
self.quantity = quantity
|
|
self.pack_type_id = pack_type_id
|
|
self.pack_embed = pack_embed
|
|
self.cost = cost
|
|
super().__init__(placeholder="Select a Cardset", options=options)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
# Import here to avoid circular imports
|
|
from api_calls import db_post
|
|
from discord_ui.confirmations import Confirm
|
|
|
|
logger.info(f"SelectBuyPacksCardset - selection: {self.values[0]}")
|
|
cardset_id = CARDSET_LABELS_TO_IDS.get(self.values[0])
|
|
if cardset_id is None:
|
|
raise ValueError(f"Unknown cardset: {self.values[0]}")
|
|
|
|
if self.values[0] == "Pokemon - Brilliant Stars":
|
|
self.pack_embed.set_image(url=IMAGES["pack-pkmnbs"])
|
|
|
|
self.pack_embed.description = (
|
|
f"{self.pack_embed.description} - {self.values[0]}"
|
|
)
|
|
view = Confirm(responders=[interaction.user], timeout=30)
|
|
await interaction.response.edit_message(
|
|
content=None, embed=self.pack_embed, view=None
|
|
)
|
|
question = await interaction.channel.send(
|
|
content=f"Your Wallet: {self.team['wallet']}₼\n"
|
|
f"Pack{'s' if self.quantity > 1 else ''} Price: {self.cost}₼\n"
|
|
f"After Purchase: {self.team['wallet'] - self.cost}₼\n\n"
|
|
f"Would you like to make this purchase?",
|
|
view=view,
|
|
)
|
|
await view.wait()
|
|
|
|
if not view.value:
|
|
await question.edit(content="Saving that money. Smart.", view=None)
|
|
return
|
|
|
|
p_model = {
|
|
"team_id": self.team["id"],
|
|
"pack_type_id": self.pack_type_id,
|
|
"pack_cardset_id": cardset_id,
|
|
}
|
|
await db_post(
|
|
"packs", payload={"packs": [p_model for x in range(self.quantity)]}
|
|
)
|
|
await db_post(f"teams/{self.team['id']}/money/-{self.cost}")
|
|
|
|
await question.edit(
|
|
content=f"{'They are' if self.quantity > 1 else 'It is'} all yours! Go rip 'em with `/open-packs`",
|
|
view=None,
|
|
)
|
|
|
|
|
|
class SelectBuyPacksTeam(discord.ui.Select):
|
|
def __init__(
|
|
self,
|
|
which: Literal["AL", "NL"],
|
|
team: dict,
|
|
quantity: int,
|
|
pack_type_id: int,
|
|
pack_embed: discord.Embed,
|
|
cost: int,
|
|
):
|
|
self.which = which
|
|
self.team = team
|
|
self.quantity = quantity
|
|
self.pack_type_id = pack_type_id
|
|
self.pack_embed = pack_embed
|
|
self.cost = cost
|
|
|
|
if which == "AL":
|
|
options = [discord.SelectOption(label=team) for team in AL_TEAMS]
|
|
else:
|
|
# Handle St. Louis Cardinals display name
|
|
options = [
|
|
discord.SelectOption(
|
|
label="St. Louis Cardinals"
|
|
if team == "St Louis Cardinals"
|
|
else team
|
|
)
|
|
for team in NL_TEAMS
|
|
]
|
|
|
|
super().__init__(placeholder=f"Select an {which} team", options=options)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
# Import here to avoid circular imports
|
|
from api_calls import db_post
|
|
from discord_ui.confirmations import Confirm
|
|
|
|
team_id = _get_team_id(self.values[0], self.which)
|
|
if team_id is None:
|
|
raise ValueError(f"Unknown team: {self.values[0]}")
|
|
|
|
self.pack_embed.description = (
|
|
f"{self.pack_embed.description} - {self.values[0]}"
|
|
)
|
|
view = Confirm(responders=[interaction.user], timeout=30)
|
|
await interaction.response.edit_message(
|
|
content=None, embed=self.pack_embed, view=None
|
|
)
|
|
question = await interaction.channel.send(
|
|
content=f"Your Wallet: {self.team['wallet']}₼\n"
|
|
f"Pack{'s' if self.quantity > 1 else ''} Price: {self.cost}₼\n"
|
|
f"After Purchase: {self.team['wallet'] - self.cost}₼\n\n"
|
|
f"Would you like to make this purchase?",
|
|
view=view,
|
|
)
|
|
await view.wait()
|
|
|
|
if not view.value:
|
|
await question.edit(content="Saving that money. Smart.", view=None)
|
|
return
|
|
|
|
p_model = {
|
|
"team_id": self.team["id"],
|
|
"pack_type_id": self.pack_type_id,
|
|
"pack_team_id": team_id,
|
|
}
|
|
await db_post(
|
|
"packs", payload={"packs": [p_model for x in range(self.quantity)]}
|
|
)
|
|
await db_post(f"teams/{self.team['id']}/money/-{self.cost}")
|
|
|
|
await question.edit(
|
|
content=f"{'They are' if self.quantity > 1 else 'It is'} all yours! Go rip 'em with `/open-packs`",
|
|
view=None,
|
|
)
|
|
|
|
|
|
class SelectUpdatePlayerTeam(discord.ui.Select):
|
|
def __init__(
|
|
self, which: Literal["AL", "NL"], player: dict, reporting_team: dict, bot
|
|
):
|
|
self.bot = bot
|
|
self.which = which
|
|
self.player = player
|
|
self.reporting_team = reporting_team
|
|
|
|
if which == "AL":
|
|
options = [discord.SelectOption(label=team) for team in AL_TEAMS]
|
|
else:
|
|
# Handle St. Louis Cardinals display name
|
|
options = [
|
|
discord.SelectOption(
|
|
label="St. Louis Cardinals"
|
|
if team == "St Louis Cardinals"
|
|
else team
|
|
)
|
|
for team in NL_TEAMS
|
|
]
|
|
|
|
super().__init__(placeholder=f"Select an {which} team", options=options)
|
|
|
|
async def callback(self, interaction: discord.Interaction):
|
|
# Import here to avoid circular imports
|
|
from api_calls import db_patch, db_post
|
|
from discord_ui.confirmations import Confirm
|
|
from helpers import player_desc, send_to_channel
|
|
|
|
# Check if already assigned - compare against both normalized franchise and full mlbclub
|
|
normalized_selection = normalize_franchise(self.values[0])
|
|
if (
|
|
normalized_selection == self.player["franchise"]
|
|
or self.values[0] == self.player["mlbclub"]
|
|
):
|
|
await interaction.response.send_message(
|
|
content=f"Thank you for the help, but it looks like somebody beat you to it! "
|
|
f"**{player_desc(self.player)}** is already assigned to the **{self.player['mlbclub']}**."
|
|
)
|
|
return
|
|
|
|
view = Confirm(responders=[interaction.user], timeout=15)
|
|
await interaction.response.edit_message(
|
|
content=f"Should I update **{player_desc(self.player)}**'s team to the **{self.values[0]}**?",
|
|
view=None,
|
|
)
|
|
question = await interaction.channel.send(content=None, view=view)
|
|
await view.wait()
|
|
|
|
if not view.value:
|
|
await question.edit(
|
|
content="That didnt't sound right to me, either. Let's not touch that.",
|
|
view=None,
|
|
)
|
|
return
|
|
else:
|
|
await question.delete()
|
|
|
|
await db_patch(
|
|
"players",
|
|
object_id=self.player["player_id"],
|
|
params=[
|
|
("mlbclub", self.values[0]),
|
|
("franchise", normalize_franchise(self.values[0])),
|
|
],
|
|
)
|
|
await db_post(f"teams/{self.reporting_team['id']}/money/25")
|
|
await send_to_channel(
|
|
self.bot,
|
|
"pd-news-ticker",
|
|
content=f"{interaction.user.name} just updated **{player_desc(self.player)}**'s team to the "
|
|
f"**{self.values[0]}**",
|
|
)
|
|
await interaction.channel.send("All done!")
|
|
|
|
|
|
class SelectView(discord.ui.View):
|
|
def __init__(self, select_objects: list[discord.ui.Select], timeout: float = 300.0):
|
|
super().__init__(timeout=timeout)
|
|
|
|
for x in select_objects:
|
|
self.add_item(x)
|