import logging import discord from typing import Coroutine, Literal from dice import ab_roll, jump_roll from exceptions import * from helpers import random_insult from in_game.gameplay_models import Game, Play, Team 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(f'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(f'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(f'checking for deletion with question and embeds') if delete_question and delete_embeds: logger.info(f'delete it all') await q_message.delete() elif delete_question: logger.info(f'delete question') await q_message.edit( content=None ) elif delete_embeds: logger.info(f'delete embeds') await q_message.edit( embeds=None ) elif return_val is None: logger.info(f'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(f'deleting message') await q_message.delete() elif return_val is None: logger.info(f'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(f'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)