Convert all `from x import *` to explicit imports in 12 files, resolving 925 F403/F405 ruff violations. Each name traced to its canonical source module. Also fixes: duplicate Session import (players.py), missing sample_team_data fixture param, duplicate method name paperdex_cardset_slash, unused commands import in package __init__ files, and Play type hint in play_lock.py. 3 pre-existing code bugs remain (F811 duplicate test names, F821 undefined `question` variable) — these need investigation, not mechanical fixes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
554 lines
20 KiB
Python
554 lines
20 KiB
Python
import logging
|
|
import discord
|
|
from typing import Literal
|
|
|
|
from dice import ab_roll, jump_roll
|
|
from exceptions import ButtonOptionNotChosen, InvalidResponder, log_exception
|
|
from helpers import random_insult
|
|
from in_game.gameplay_models import Play
|
|
|
|
logger = logging.getLogger("discord_app")
|
|
|
|
|
|
# def check_responder(func):
|
|
# def wrap(*args, **kwargs):
|
|
# try:
|
|
# result = func(*args, **kwargs)
|
|
# except Exc
|
|
|
|
|
|
class Confirm(discord.ui.View):
|
|
def __init__(
|
|
self,
|
|
responders: list,
|
|
timeout: float = 300.0,
|
|
label_type: Literal["yes", "confirm"] = "confirm",
|
|
):
|
|
super().__init__(timeout=timeout)
|
|
if not isinstance(responders, list):
|
|
raise TypeError("responders must be a list")
|
|
self.value = None
|
|
self.responders = responders
|
|
if label_type == "yes":
|
|
self.confirm.label = "Yes"
|
|
self.cancel.label = "No"
|
|
|
|
# When the confirm button is pressed, set the inner value to `True` and
|
|
# stop the View from listening to more input.
|
|
# We also send the user an ephemeral message that we're confirming their choice.
|
|
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.green)
|
|
async def confirm(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
if interaction.user not in self.responders:
|
|
await interaction.response.send_message(
|
|
content="Get out of here", ephemeral=True, delete_after=10.0
|
|
)
|
|
|
|
self.value = True
|
|
self.clear_items()
|
|
self.stop()
|
|
|
|
# This one is similar to the confirmation button except sets the inner value to `False`
|
|
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.grey)
|
|
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
if interaction.user not in self.responders:
|
|
await interaction.response.send_message(
|
|
content="Get out of here", ephemeral=True, delete_after=10.0
|
|
)
|
|
|
|
self.value = False
|
|
self.clear_items()
|
|
self.stop()
|
|
|
|
|
|
class ButtonOptions(discord.ui.View):
|
|
def __init__(
|
|
self,
|
|
labels: list[str],
|
|
responders: list,
|
|
timeout: float = 300.0,
|
|
disable_chosen: bool = False,
|
|
):
|
|
logger.info(
|
|
f"ButtonOptions - labels: {labels} / responders: {responders} / timeout: {timeout} / disable_chosen: {disable_chosen}"
|
|
)
|
|
|
|
super().__init__(timeout=timeout)
|
|
if not isinstance(responders, list):
|
|
raise TypeError("responders must be a list")
|
|
if len(labels) > 5 or len(labels) < 1:
|
|
log_exception(ValueError, "ButtonOptions support between 1 and 5 options")
|
|
|
|
self.value = None
|
|
self.responders = responders
|
|
self.options = labels
|
|
self.disable_chosen = disable_chosen
|
|
# if len(labels) == 5:
|
|
# for count, x in enumerate(labels):
|
|
# if count == 0:
|
|
# self.option1.label = x
|
|
# if x is None or x.lower() == 'na' or x == 'N/A':
|
|
# self.remove_item(self.option1)
|
|
# if count == 1:
|
|
# self.option2.label = x
|
|
# if x is None or x.lower() == 'na' or x == 'N/A':
|
|
# self.remove_item(self.option2)
|
|
# if count == 2:
|
|
# self.option3.label = x
|
|
# if x is None or x.lower() == 'na' or x == 'N/A':
|
|
# self.remove_item(self.option3)
|
|
# if count == 3:
|
|
# self.option4.label = x
|
|
# if x is None or x.lower() == 'na' or x == 'N/A':
|
|
# self.remove_item(self.option4)
|
|
# if count == 4:
|
|
# self.option5.label = x
|
|
# if x is None or x.lower() == 'na' or x == 'N/A':
|
|
# self.remove_item(self.option5)
|
|
|
|
# else:
|
|
all_options = [
|
|
self.option1,
|
|
self.option2,
|
|
self.option3,
|
|
self.option4,
|
|
self.option5,
|
|
]
|
|
logger.info(f"all_options: {all_options}")
|
|
for count, x in enumerate(labels):
|
|
if x is None or x.lower() == "na" or x.lower() == "n/a":
|
|
self.remove_item(all_options[count])
|
|
else:
|
|
all_options[count].label = x
|
|
|
|
if len(labels) < 2:
|
|
self.remove_item(self.option2)
|
|
if len(labels) < 3:
|
|
self.remove_item(self.option3)
|
|
if len(labels) < 4:
|
|
self.remove_item(self.option4)
|
|
if len(labels) < 5:
|
|
self.remove_item(self.option5)
|
|
|
|
@discord.ui.button(label="Option 1", style=discord.ButtonStyle.primary)
|
|
async def option1(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
if interaction.user not in self.responders:
|
|
await interaction.response.send_message(
|
|
content="Get out of here", ephemeral=True, delete_after=10.0
|
|
)
|
|
|
|
self.stop()
|
|
if self.disable_chosen:
|
|
button.disabled = True
|
|
self.value = self.options[0]
|
|
await interaction.edit_original_response(view=self)
|
|
|
|
@discord.ui.button(label="Option 2", style=discord.ButtonStyle.primary)
|
|
async def option2(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
if interaction.user not in self.responders:
|
|
await interaction.response.send_message(
|
|
content="Get out of here", ephemeral=True, delete_after=10.0
|
|
)
|
|
|
|
self.stop()
|
|
if self.disable_chosen:
|
|
button.disabled = True
|
|
self.value = self.options[1]
|
|
await interaction.edit_original_response(view=self)
|
|
|
|
@discord.ui.button(label="Option 3", style=discord.ButtonStyle.primary)
|
|
async def option3(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
if interaction.user not in self.responders:
|
|
await interaction.response.send_message(
|
|
content="Get out of here", ephemeral=True, delete_after=10.0
|
|
)
|
|
|
|
self.stop()
|
|
if self.disable_chosen:
|
|
button.disabled = True
|
|
self.value = self.options[2]
|
|
await interaction.edit_original_response(view=self)
|
|
|
|
@discord.ui.button(label="Option 4", style=discord.ButtonStyle.primary)
|
|
async def option4(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
if interaction.user not in self.responders:
|
|
await interaction.response.send_message(
|
|
content="Get out of here", ephemeral=True, delete_after=10.0
|
|
)
|
|
|
|
self.stop()
|
|
if self.disable_chosen:
|
|
button.disabled = True
|
|
self.value = self.options[3]
|
|
await interaction.edit_original_response(view=self)
|
|
|
|
@discord.ui.button(label="Option 5", style=discord.ButtonStyle.primary)
|
|
async def option5(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
if interaction.user not in self.responders:
|
|
await interaction.response.send_message(
|
|
content="Get out of here", ephemeral=True, delete_after=10.0
|
|
)
|
|
|
|
self.stop()
|
|
if self.disable_chosen:
|
|
button.disabled = True
|
|
self.value = self.options[4]
|
|
await interaction.edit_original_response(view=self)
|
|
|
|
|
|
class SelectPosition(discord.ui.View):
|
|
def __init__(self, *, timeout: int = 30, responders: list[discord.User]):
|
|
if not isinstance(responders, list):
|
|
raise TypeError("responders must be a list")
|
|
|
|
super().__init__(timeout=timeout)
|
|
|
|
self.value = None
|
|
self.responders = responders
|
|
|
|
async def check_responders(self, interaction: discord.Interaction):
|
|
if interaction.user not in self.responders:
|
|
await interaction.response.send_message(
|
|
content=random_insult(), ephemeral=True, delete_after=10.0
|
|
)
|
|
log_exception(
|
|
InvalidResponder,
|
|
f"{interaction.user.name} not in responders: {self.responders}",
|
|
)
|
|
|
|
async def button_press(self, interaction: discord.Interaction, this_position: str):
|
|
await self.check_responders(interaction)
|
|
self.stop()
|
|
self.value = this_position
|
|
await interaction.edit_original_response(view=self)
|
|
|
|
@discord.ui.button(label="LF", style=discord.ButtonStyle.blurple, row=0)
|
|
async def left_field(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="CF", style=discord.ButtonStyle.blurple, row=0)
|
|
async def center_field(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="RF", style=discord.ButtonStyle.blurple, row=0)
|
|
async def right_field(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="3B", style=discord.ButtonStyle.green, row=1)
|
|
async def third_base(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="SS", style=discord.ButtonStyle.green, row=1)
|
|
async def shortstop(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="2B", style=discord.ButtonStyle.green, row=1)
|
|
async def second_base(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="1B", style=discord.ButtonStyle.green, row=1)
|
|
async def first_base(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="C", style=discord.ButtonStyle.gray, row=2)
|
|
async def catcher(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="P", style=discord.ButtonStyle.gray, row=2)
|
|
async def pitcher(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
@discord.ui.button(label="PH/PR", style=discord.ButtonStyle.gray, row=2)
|
|
async def pinch_hitter(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
await self.button_press(interaction, button.label)
|
|
|
|
|
|
async def ask_position(interaction: discord.Interaction):
|
|
view = SelectPosition(responders=[interaction.user], timeout=15)
|
|
|
|
p_message = await interaction.channel.send(
|
|
content="Please select a position", view=view
|
|
)
|
|
await view.wait()
|
|
|
|
if view.value:
|
|
await p_message.delete()
|
|
return view.value
|
|
|
|
else:
|
|
await p_message.edit(
|
|
content="To move things along, I will set the position to PH.", view=None
|
|
)
|
|
return "PH"
|
|
|
|
|
|
async def ask_confirm(
|
|
interaction: discord.Interaction,
|
|
question: str,
|
|
label_type: Literal["yes", "confirm"] = "confirm",
|
|
timeout: int = 60,
|
|
delete_question: bool = True,
|
|
custom_confirm_label: str = None,
|
|
custom_cancel_label: str = None,
|
|
embed: discord.Embed = None,
|
|
delete_embed: bool = False,
|
|
) -> bool:
|
|
"""
|
|
button_callbacks: keys are button values, values are async functions
|
|
"""
|
|
try:
|
|
view = Confirm(
|
|
responders=[interaction.user], timeout=timeout, label_type=label_type
|
|
)
|
|
except AttributeError:
|
|
view = Confirm(
|
|
responders=[interaction.author], timeout=timeout, label_type=label_type
|
|
)
|
|
if custom_confirm_label:
|
|
view.confirm.label = custom_confirm_label
|
|
if custom_cancel_label:
|
|
view.cancel.label = custom_cancel_label
|
|
|
|
q_message = await interaction.channel.send(question, view=view)
|
|
await view.wait()
|
|
|
|
if view.value:
|
|
if delete_question:
|
|
await q_message.delete()
|
|
else:
|
|
await q_message.edit(content=question, view=None)
|
|
return True
|
|
|
|
else:
|
|
if delete_question:
|
|
await q_message.delete()
|
|
else:
|
|
await q_message.edit(content=question, view=None)
|
|
return False
|
|
|
|
|
|
async def ask_with_buttons(
|
|
interaction: discord.Interaction,
|
|
button_options: list[str],
|
|
question: str = None,
|
|
timeout: int = 60,
|
|
delete_question: bool = True,
|
|
embeds: list[discord.Embed] = None,
|
|
delete_embeds: bool = False,
|
|
edit_original_interaction: bool = False,
|
|
none_okay: bool = False,
|
|
confirmation_message: str = None,
|
|
) -> str:
|
|
"""
|
|
Returns text of button pressed
|
|
"""
|
|
logger.info(
|
|
f"ask_with_buttons - button_options: {button_options} / question: {question} / timeout: {timeout} / delete_question: {delete_question} / embeds: {embeds} / delete_embeds: {delete_embeds} / edit_original_transaction: {edit_original_interaction} / confirmation_message: {confirmation_message}"
|
|
)
|
|
|
|
if question is None and embeds is None:
|
|
log_exception(KeyError, "At least one of question or embed must be provided")
|
|
|
|
if confirmation_message is not None and (delete_question or delete_embeds):
|
|
log_exception(
|
|
KeyError,
|
|
"Posting a confirmation message is not supported while deleting the message or embeds",
|
|
)
|
|
|
|
view = ButtonOptions(
|
|
responders=[interaction.user], timeout=timeout, labels=button_options
|
|
)
|
|
logger.info(f"view: {view}")
|
|
# if edit_original_interaction:
|
|
# logger.info(f'editing message')
|
|
# q_message = await interaction.edit_original_response(
|
|
# content=question,
|
|
# view=view,
|
|
# embeds=embeds
|
|
# )
|
|
# logger.info(f'edited')
|
|
# else:
|
|
# logger.info(f'posting message')
|
|
# q_message = await interaction.channel.send(
|
|
# content=question,
|
|
# view=view,
|
|
# embeds=embeds
|
|
# )
|
|
logger.info("posting message")
|
|
if edit_original_interaction:
|
|
q_message = await interaction.edit_original_response(
|
|
content=question, view=view, embeds=embeds
|
|
)
|
|
else:
|
|
q_message = await interaction.channel.send(
|
|
content=question, view=view, embeds=embeds
|
|
)
|
|
logger.info("waiting for response")
|
|
await view.wait()
|
|
|
|
if view.value:
|
|
return_val = view.value
|
|
|
|
else:
|
|
return_val = None
|
|
logger.info(f"return_val: {return_val}")
|
|
|
|
if question is not None and embeds is not None:
|
|
logger.info("checking for deletion with question and embeds")
|
|
if delete_question and delete_embeds:
|
|
logger.info("delete it all")
|
|
await q_message.delete()
|
|
elif delete_question:
|
|
logger.info("delete question")
|
|
await q_message.edit(content=None)
|
|
elif delete_embeds:
|
|
logger.info("delete embeds")
|
|
await q_message.edit(embeds=None)
|
|
elif return_val is None:
|
|
logger.info("remove view")
|
|
new_content = (
|
|
confirmation_message if confirmation_message is not None else question
|
|
)
|
|
logger.info(f"Confirmation message: {new_content}")
|
|
await q_message.edit(content=new_content, view=None)
|
|
|
|
elif (question is not None and delete_question) or (
|
|
embeds is not None and delete_embeds
|
|
):
|
|
logger.info("deleting message")
|
|
await q_message.delete()
|
|
|
|
elif return_val is None:
|
|
logger.info("No reponse, remove view")
|
|
await q_message.edit(view=None)
|
|
|
|
if return_val is not None or none_okay:
|
|
logger.info(f"Returning: {return_val}")
|
|
if confirmation_message is not None:
|
|
new_content = (
|
|
confirmation_message if confirmation_message is not None else question
|
|
)
|
|
logger.info(f"Confirmation message: {new_content}")
|
|
await q_message.edit(content=new_content, view=None)
|
|
return return_val
|
|
|
|
log_exception(ButtonOptionNotChosen, "Selecting an option is mandatory")
|
|
|
|
|
|
class ScorebugButtons(discord.ui.View):
|
|
def __init__(self, play: Play, embed: discord.Embed, timeout: float = 30):
|
|
super().__init__(timeout=timeout)
|
|
self.value = None
|
|
self.batting_team = play.batter.team
|
|
self.pitching_team = play.pitcher.team
|
|
self.pitcher_card_url = play.pitcher.player.pitcher_card_url
|
|
self.batter_card_url = play.batter.player.batter_card_url
|
|
self.team = play.batter.team
|
|
self.play = play
|
|
self.had_chaos = False
|
|
self.embed = embed
|
|
|
|
if play.on_base_code == 0:
|
|
self.remove_item(self.button_jump)
|
|
|
|
async def interaction_check(
|
|
self, interaction: discord.Interaction[discord.Client]
|
|
) -> bool:
|
|
logger.info(
|
|
f"user id: {interaction.user.id} / batting_team: {self.batting_team}"
|
|
)
|
|
if interaction.user.id == self.batting_team.gmid:
|
|
logger.info(
|
|
f"User {interaction.user.id} rolling in Game {self.play.game.id}"
|
|
)
|
|
return True
|
|
elif self.batting_team.is_ai and interaction.user.id == self.pitching_team.gmid:
|
|
logger.info(
|
|
f"User {interaction.user.id} rolling for AI in Game {self.play.game.id}"
|
|
)
|
|
return True
|
|
|
|
logger.info(f"User {interaction.user.id} rejected in Game {self.play.game.id}")
|
|
await interaction.response.send_message(
|
|
content="Get out of here", ephemeral=True, delete_after=5.0
|
|
)
|
|
return False
|
|
|
|
# async def on_timeout(self) -> Coroutine[Any, Any, None]:
|
|
# await self.interaction
|
|
|
|
@discord.ui.button(label="Roll AB", style=discord.ButtonStyle.primary)
|
|
async def button_ab(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
logger.info(
|
|
f"User {interaction.user.id} rolling AB in Game {self.play.game.id}"
|
|
)
|
|
|
|
this_roll = ab_roll(
|
|
self.team,
|
|
self.play.game,
|
|
allow_chaos=not self.had_chaos and self.play.on_base_code > 0,
|
|
)
|
|
logger.info(f"this_roll: {this_roll}")
|
|
if this_roll.is_chaos:
|
|
logger.info("AB Roll Is Chaos")
|
|
self.had_chaos = True
|
|
else:
|
|
button.disabled = True
|
|
|
|
await interaction.channel.send(content=None, embeds=this_roll.embeds)
|
|
|
|
if this_roll.d_six_one is not None and this_roll.d_six_one > 3:
|
|
logger.info("ScorebugButton - updating embed card to pitcher")
|
|
self.embed.set_image(url=self.pitcher_card_url)
|
|
logger.debug(f"embed image url: {self.embed.image}")
|
|
|
|
logger.debug(f"new embed: {self.embed}")
|
|
await interaction.response.edit_message(view=self, embed=self.embed)
|
|
|
|
@discord.ui.button(label="Check Jump", style=discord.ButtonStyle.secondary)
|
|
async def button_jump(
|
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
|
):
|
|
logger.info(
|
|
f"User {interaction.user.id} rolling jump in Game {self.play.game.id}"
|
|
)
|
|
|
|
this_roll = jump_roll(self.team, self.play.game)
|
|
button.disabled = True
|
|
|
|
await interaction.channel.send(content=None, embeds=this_roll.embeds)
|
|
await interaction.response.edit_message(view=self)
|