from typing import Optional import discord from discord.ui.text_input import TextInput, TextStyle from meta import LionBot from meta.errors import UserInputError from core.data import RankType from utils.ui import FastModal, error_handler_for, ModalRetryUI from utils.lib import parse_duration, replace_multiple from .. import babel, logger from ..data import AnyRankData from ..utils import format_stat_range, rank_model_from_type, rank_message_keys _p = babel._p class RankEditor(FastModal): """ Create or edit a single Rank. """ role_name: TextInput = TextInput( label='ROLE_NAME_PLACHOLDER', max_length=128, required=True ) def role_name_setup(self): self.role_name.label = self.bot.translator.t(_p( 'ui:rank_editor|input:role_name|label', "Role Name" )) self.role_name.placeholder = self.bot.translator.t(_p( 'ui:rank_editor|input:role_name|placeholder', "Name of the awarded guild role" )) def role_name_parse(self) -> str: return self.role_name.value role_colour: TextInput = TextInput( label='ROLE_COLOUR_PLACEHOLDER', min_length=7, max_length=16, required=False ) def role_colour_setup(self): self.role_colour.label = self.bot.translator.t(_p( 'ui:rank_editor|input:role_colour|label', "Role Colour" )) self.role_colour.placeholder = self.bot.translator.t(_p( 'ui:rank_editor|input:role_colour|placeholder', "Colour of the awarded guild role, e.g. #AB1321" )) def role_colour_parse(self) -> discord.Colour: t = self.bot.translator.t if self.role_colour.value: try: colour = discord.Colour.from_str(self.role_colour.value) except ValueError: raise UserInputError( _msg=t(_p( 'ui:rank_editor|input:role_colour|error:parse', "`role_colour`: Could not parse colour! Please use `#` format e.g. `#AB1325`." )) ) else: # TODO: Could use a standardised spectrum # And use the required value to select a colour colour = discord.Colour.random() return colour requires: TextInput = TextInput( label='REQUIRES_PLACEHOLDER', max_length=9, required=True, ) def requires_setup(self): if self.rank_type is RankType.VOICE: self.requires.label = self.bot.translator.t(_p( 'ui:rank_editor|type:voice|input:requires|label', "Required Voice Hours" )) self.requires.placholder = self.bot.translator.t(_p( 'ui:rank_editor|type:voice|input:requires|placeholder', "Number of voice hours before awarding this rank" )) elif self.rank_type is RankType.XP: self.requires.label = self.bot.translator.t(_p( 'ui:rank_editor|type:xp|input:requires|label', "Required XP" )) self.requires.placholder = self.bot.translator.t(_p( 'ui:rank_editor|type:xp|input:requires|placeholder', "Amount of XP needed before obtaining this rank" )) elif self.rank_type is RankType.MESSAGE: self.requires.label = self.bot.translator.t(_p( 'ui:rank_editor|type:message|input:requires|label', "Required Message Count" )) self.requires.placholder = self.bot.translator.t(_p( 'ui:rank_editor|type:message|input:requires|placeholder', "Number of messages needed before awarding rank" )) def requires_parse(self) -> int: t = self.bot.translator.t value = self.requires.value # TODO: Bound checking and errors for each type if self.rank_type is RankType.VOICE: if value.isdigit(): data = int(value) * 3600 else: data = parse_duration(self.requires.value) if not data: raise UserInputError( _msg=t(_p( 'ui:rank_editor|type:voice|input:requires|error:parse', "`requires`: Could not parse provided minimum time! Please write a number of hours." )) ) elif self.rank_type is RankType.MESSAGE: value = value.lower().strip(' messages') if value.isdigit(): data = int(value) else: raise UserInputError( _msg=t(_p( 'ui:rank_editor|type:message|input:requires|error:parse', "`requires`: Could not parse provided minimum message count! Please enter an integer." )) ) elif self.rank_type is RankType.XP: value = value.lower().strip(' xps') if value.isdigit(): data = int(value) else: raise UserInputError( _msg=t(_p( 'ui:rank_editor|type:xp|input:requires|error:parse', "`requires`: Could not parse provided minimum XP! Please enter an integer." )) ) return data reward: TextInput = TextInput( label='REWARD_PLACEHOLDER', max_length=9, required=False ) def reward_setup(self): self.reward.label = self.bot.translator.t(_p( 'ui:rank_editor|input:reward|label', "LionCoins awarded upon achieving this rank" )) self.reward.placeholder = self.bot.translator.t(_p( 'ui:rank_editor|input:reward|placeholder', "LionCoins awarded upon achieving this rank" )) def reward_parse(self) -> int: t = self.bot.translator.t value = self.reward.value if not value: # Empty value data = 0 elif value.isdigit(): data = int(value) else: raise UserInputError( _msg=t(_p( 'ui:rank_editor|input:reward|error:parse', '`reward`: Please enter an integer number of LionCoins.' )) ) return data message: TextInput = TextInput( label='MESSAGE_PLACEHOLDER', style=TextStyle.long, max_length=1024, required=True ) def message_setup(self): t = self.bot.translator.t self.message.label = t(_p( 'ui:rank_editor|input:message|label', "Rank Message" )) self.message.placeholder = t(_p( 'ui:rank_editor|input:message|placeholder', "Congratulatory message sent to the user upon achieving this rank." )) if self.rank_type is RankType.VOICE: # TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key msg_default = t(_p( 'ui:rank_editor|input:message|default|type:voice', "Congratulations {user_mention}!\n" "For working hard for **{requires}**, you have achieved the rank of " "**{role_name}** in **{guild_name}**! Keep up the good work." )) elif self.rank_type is RankType.XP: # TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key msg_default = t(_p( 'ui:rank_editor|input:message|default|type:xp', ( "Congratulations {user_mention}!\n" "For earning **{requires}**, you have achieved the guild rank of " "**{role_name}** in **{guild_name}**!" ) )) elif self.rank_type is RankType.MESSAGE: # TRANSLATOR NOTE: Don't change the keys here, they will be automatically replaced by the localised key msg_default = t(_p( 'ui:rank_editor|input:message|default|type:msg', ( "Congratulations {user_mention}!\n" "For sending **{requires}**, you have achieved the guild rank of " "**{role_name}** in **{guild_name}**!" ) )) # Replace the progam keys in the default message with the correct localised keys. replace_map = {pkey: t(lkey) for pkey, lkey in rank_message_keys} self.message.default = replace_multiple(msg_default, replace_map) def message_parse(self) -> str: # Replace the localised keys with programmatic keys t = self.bot.translator.t replace_map = {t(lkey): pkey for pkey, lkey in rank_message_keys} return replace_multiple(self.message.value, replace_map) def __init__(self, bot: LionBot, rank_type: RankType, **kwargs): self.bot = bot self.rank_type = rank_type self.message_setup() self.reward_setup() self.requires_setup() self.role_name_setup() self.role_colour_setup() super().__init__(**kwargs) @classmethod async def edit_rank(cls, interaction: discord.Interaction, rank_type: RankType, rank: AnyRankData, role: discord.Role, callback=None): bot = interaction.client self = cls( bot, rank_type, title=bot.translator.t(_p('ui:rank_editor|mode:edit|title', "Rank Editor")) ) self.role_name.default = role.name self.role_colour.default = str(role.colour) self.requires.default = format_stat_range(rank_type, rank.required, None, short=True) self.reward.default = rank.reward if rank.message: t = bot.translator.t replace_map = {pkey: t(lkey) for pkey, lkey in rank_message_keys} self.message.default = replace_multiple(rank.message, replace_map) @self.submit_callback(timeout=15*60) async def _edit_rank_callback(interaction): # Parse each field in turn # A parse error will raise UserInputError and trigger ModalRetry role_name = self.role_name_parse() role_colour = self.role_colour_parse() requires = self.requires_parse() reward = self.reward_parse() message = self.message_parse() # Once successful, use rank.update() to edit the rank if modified, if requires != rank.required or reward != rank.reward or message != rank.message: # In the corner-case where the rank has been externally deleted, this will be a no-op await rank.update( required=requires, reward=reward, message=message ) self.bot.get_cog('RankCog').flush_guild_ranks(interaction.guild.id) # and edit the role with role.edit() if modified. if role_name != role.name or role_colour != role.colour: await role.edit(name=role_name, colour=role_colour) # Respond with an update ack.. # (Might not be required? Or maybe use ephemeral ack?) # Finally, run the provided parent callback if provided if callback is not None: await callback(rank, interaction) # Editor ready, now send await interaction.response.send_modal(self) return self @classmethod async def create_rank(cls, interaction: discord.Interaction, rank_type: RankType, guild: discord.Guild, role: Optional[discord.Role] = None, callback=None): bot = interaction.client self = cls( bot, rank_type, title=bot.translator.t(_p( 'ui:rank_editor|mode:create|title', "Rank Creator" )) ) if role is not None: self.role_name.default = role.name self.role_colour.default = str(role.colour) @self.submit_callback(timeout=15*60) async def _create_rank_callback(interaction): # Parse each field in turn # A parse error will raise UserInputError and trigger ModalRetry role_name = self.role_name_parse() role_colour = self.role_colour_parse() requires = self.requires_parse() reward = self.reward_parse() message = self.message_parse() # Create or edit the role if role is not None: rank_role = role # Edit role if properties were updated if (role_name != role.name or role_colour != role.colour): await role.edit(name=role_name, colour=role_colour) else: # Create the role rank_role = await guild.create_role(name=role_name, colour=role_colour) # TODO: Move role to correct position, based on rank list # Create the Rank model = rank_model_from_type(rank_type) rank = await model.create( roleid=rank_role.id, guildid=guild.id, required=requires, reward=reward, message=message ) self.bot.get_cog('RankCog').flush_guild_ranks(guild.id) if callback is not None: await callback(rank, interaction) # Editor ready, now send await interaction.response.send_modal(self) return self @error_handler_for(UserInputError) async def rerequest(self, interaction: discord.Interaction, error: UserInputError): await ModalRetryUI(self, error.msg).respond_to(interaction)