Files
croccybot/src/modules/ranks/ui/editor.py
2023-05-25 16:45:54 +03:00

366 lines
14 KiB
Python

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 `#<hex>` 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)