Ruby-Cogs/extendedeconomy/views/cost_menu.py
2025-05-23 02:30:00 -04:00

413 lines
16 KiB
Python

import contextlib
import logging
import math
import typing as t
from contextlib import suppress
import discord
from redbot.core import commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator
from ..abc import MixinMeta
from ..common.models import CommandCost
from ..common.utils import format_command_txt, format_settings
log = logging.getLogger("red.vrt.extendedeconomy.admin")
_ = Translator("ExtendedEconomy", __file__)
PER_PAGE = 2
LEFT = "\N{LEFTWARDS BLACK ARROW}\N{VARIATION SELECTOR-16}"
LEFT10 = "\N{BLACK LEFT-POINTING DOUBLE TRIANGLE}"
RIGHT = "\N{BLACK RIGHTWARDS ARROW}\N{VARIATION SELECTOR-16}"
RIGHT10 = "\N{BLACK RIGHT-POINTING DOUBLE TRIANGLE}"
UP = "\N{UPWARDS BLACK ARROW}"
DOWN = "\N{DOWNWARDS BLACK ARROW}"
CLOSE = "\N{HEAVY MULTIPLICATION X}\N{VARIATION SELECTOR-16}"
ADD = "\N{HEAVY PLUS SIGN}"
REMOVE = "\N{HEAVY MINUS SIGN}"
EDIT = "\N{PENCIL}"
class CostModal(discord.ui.Modal):
def __init__(self, title: str, data: t.Optional[dict] = None, add: t.Optional[bool] = False):
super().__init__(title=title, timeout=120)
if data is None:
data = {}
self.add = add
if add:
self.command = discord.ui.TextInput(
label=_("Command Name"),
placeholder="ping",
default=data.get("command"),
)
self.add_item(self.command)
self.cost = discord.ui.TextInput(
label=_("Cost"),
placeholder="100",
default=data.get("cost"),
)
self.add_item(self.cost)
self.duration = discord.ui.TextInput(
label=_("Duration (Seconds)"),
placeholder="3600",
default=data.get("duration", "3600"),
)
self.add_item(self.duration)
self.details = discord.ui.TextInput(
label=_("level, prompt, modifier (Comma separated)"),
placeholder="all, notify, static",
default=data.get("details", "all, notify, static"),
)
self.add_item(self.details)
self.value = discord.ui.TextInput(
label=_("Value (Decimal)[Optional]"),
placeholder="0.0",
default=data.get("value", "0.0"),
required=False,
)
self.add_item(self.value)
self.data = {}
async def try_respond(self, interaction: discord.Interaction, content: str):
try:
await interaction.response.send_message(content, ephemeral=True)
except discord.HTTPException:
with suppress(discord.NotFound):
await interaction.followup.send(content, ephemeral=True)
async def on_submit(self, interaction: discord.Interaction):
try:
self.data["cost"] = int(self.cost.value)
except ValueError:
return await self.try_respond(interaction, _("Cost must be an integer!"))
try:
self.data["duration"] = int(self.duration.value)
except ValueError:
return await self.try_respond(interaction, _("Duration must be an integer!"))
try:
self.data["value"] = float(self.value.value)
except ValueError:
return await self.try_respond(interaction, _("Value must be a decimal!"))
txt = self.details.value
if "," in txt:
i = [x.strip() for x in txt.split(",")]
else:
i = [x.strip() for x in txt.split()]
if len(i) != 3:
txt = _("Invalid details! Must be 3 values separated by commas.")
txt += _("\nExample: `user, notify, static`")
return await self.try_respond(interaction, txt)
level, prompt, modifier = i
if level not in ["admin", "mod", "all", "user", "global"]:
return await self.try_respond(
interaction, _("Invalid level! You must use one of: admin, mod, all, user, global")
)
if prompt not in ["text", "reaction", "button", "silent", "notify"]:
return await self.try_respond(
interaction, _("Invalid prompt! You must use one of: text, reaction, button, silent, notify")
)
if modifier not in ["static", "percent", "exponential", "linear"]:
return await self.try_respond(
interaction, _("Invalid modifier! You must use one of: static, percent, exponential, linear")
)
self.data["level"] = level
self.data["prompt"] = prompt
self.data["modifier"] = modifier
if self.add:
self.data["command"] = self.command.value
with suppress(discord.NotFound):
await interaction.response.defer()
self.stop()
async def on_timeout(self) -> None:
self.stop()
return await super().on_timeout()
async def on_error(self, interaction: discord.Interaction, error: Exception, /) -> None:
txt = f"Modal failed for {interaction.user.name}!\n" f"Guild: {interaction.guild}\n" f"Title: {self.title}\n"
log.error(txt, exc_info=error)
class MenuButton(discord.ui.Button):
def __init__(
self,
func: t.Callable,
emoji: t.Optional[t.Union[str, discord.Emoji, discord.PartialEmoji]] = None,
style: t.Optional[discord.ButtonStyle] = discord.ButtonStyle.primary,
label: t.Optional[str] = None,
disabled: t.Optional[bool] = False,
row: t.Optional[int] = None,
):
super().__init__(style=style, label=label, disabled=disabled, emoji=emoji, row=row)
self.func = func
async def callback(self, interaction: discord.Interaction):
await self.func(interaction, self)
class CostMenu(discord.ui.View):
def __init__(self, ctx: commands.Context, cog: MixinMeta, global_bank: bool, check: t.Callable):
super().__init__(timeout=240)
self.ctx = ctx
self.author = ctx.author
self.cog = cog
self.bot: Red = cog.bot
self.db = cog.db
self.global_bank = global_bank
self.check = check
self.message: discord.Message = None
self.page: int = 0
self.selected: int = 0 # Which command cost field is currently selected
self.pages: t.List[discord.Embed] = self.get_pages()
self.b = {
"add": MenuButton(self.add, ADD, style=discord.ButtonStyle.success, row=1),
"up": MenuButton(self.up, UP, row=1),
"remove": MenuButton(self.remove, REMOVE, style=discord.ButtonStyle.danger, row=1),
"left": MenuButton(self.left, LEFT, row=2),
"edit": MenuButton(self.edit, EDIT, style=discord.ButtonStyle.secondary, row=2),
"right": MenuButton(self.right, RIGHT, row=2),
"left10": MenuButton(self.left10, LEFT10, row=3),
"down": MenuButton(self.down, DOWN, row=3),
"right10": MenuButton(self.right10, RIGHT10, row=3),
"close": MenuButton(self.close, CLOSE, style=discord.ButtonStyle.danger, row=4),
}
async def interaction_check(self, interaction: discord.Interaction):
if interaction.user.id != self.author.id:
await interaction.response.send_message(_("This isn't your menu!"), ephemeral=True)
return False
return True
async def on_timeout(self) -> None:
if self.message:
with contextlib.suppress(Exception):
await self.message.edit(view=None)
await self.ctx.tick()
def get_costs(self) -> t.Dict[str, CommandCost]:
if self.global_bank:
return self.db.command_costs
return self.db.get_conf(self.ctx.guild).command_costs
def get_command_name(self) -> str:
page = self.pages[self.page]
field = page.fields[self.selected]
return field.name.replace("", "")
def get_cost_obj(self) -> t.Union[CommandCost, None]:
costs = self.get_costs()
command_name = self.get_command_name()
cost_obj = costs.get(command_name)
return cost_obj
async def refresh(self):
self.pages = self.get_pages()
self.clear_items()
if self.get_costs():
for b in self.b.values():
b.disabled = False
self.add_item(b)
else:
for k, b in self.b.items():
if k in ["add", "close"]:
self.add_item(b)
else:
b.disabled = True
self.add_item(b)
page: discord.Embed = self.pages[self.page]
if self.selected >= len(page.fields) and page.fields:
# Place the arrow on the last field if selected is out of bounds
self.selected = len(page.fields) - 1
page.set_field_at(
self.selected,
name=f"{page.fields[self.selected].name}",
value=page.fields[self.selected].value,
inline=False,
)
if self.message:
await self.message.edit(embed=page, view=self)
else:
self.message = await self.ctx.send(embed=page, view=self)
def get_pages(self):
guildconf = self.db.get_conf(self.ctx.guild)
conf = self.db if self.global_bank else guildconf
itemized: t.List[t.Tuple[str, CommandCost]] = list(conf.command_costs.items())
itemized.sort(key=lambda x: x[0])
start, stop = 0, PER_PAGE
pages = []
page_count = math.ceil(len(conf.command_costs) / PER_PAGE)
base_embed = format_settings(
self.db,
guildconf,
self.global_bank,
self.author.id in self.bot.owner_ids,
self.db.delete_after,
)
for p in range(page_count):
embed = base_embed.copy()
embed.set_footer(text=_("Page {}/{}").format(p + 1, page_count))
stop = min(stop, len(conf.command_costs))
for i in range(start, stop):
command_name, cost_obj = itemized[i]
txt = format_command_txt(cost_obj)
is_selected = i % PER_PAGE == self.selected
name = f"{command_name}" if is_selected else command_name
embed.add_field(name=name, value=txt, inline=False)
pages.append(embed)
start += PER_PAGE
stop += PER_PAGE
if not pages:
pages.append(base_embed)
return pages
# ROW 1
async def add(self, interaction: discord.Interaction, button: discord.ui.Button):
modal = CostModal(_("Add Command Cost"), add=True)
await interaction.response.send_modal(modal)
await modal.wait()
if not modal.data:
return
command_name = modal.data["command"]
if command_name in self.get_costs():
return await interaction.followup.send(_("Command already has a cost!"), ephemeral=True)
command_obj = self.bot.get_command(command_name)
if not command_obj:
command_obj = self.bot.tree.get_command(command_name)
if not command_obj:
return await interaction.followup.send(_("Command not found!"), ephemeral=True)
if isinstance(command_obj, commands.commands._AlwaysAvailableCommand):
txt = _("You can't add a cost to a command that is always available!")
return await interaction.followup.send(txt, ephemeral=True)
if isinstance(command_obj, (commands.Command, commands.HybridCommand)):
if (command_obj.requires.privilege_level or 0) > await commands.requires.PrivilegeLevel.from_ctx(self.ctx):
txt = _("You can't add costs to commands you don't have permission to run!")
return await interaction.followup.send(txt, ephemeral=True)
cost_obj = CommandCost(
cost=modal.data["cost"],
duration=modal.data["duration"],
level=modal.data["level"],
prompt=modal.data["prompt"],
modifier=modal.data["modifier"],
value=modal.data["value"],
)
if self.global_bank:
self.cog.db.command_costs[command_name] = cost_obj
else:
conf = self.cog.db.get_conf(self.ctx.guild)
conf.command_costs[command_name] = cost_obj
msg = await interaction.followup.send(_("Command cost added!"), ephemeral=True)
if msg:
await msg.delete(delay=10)
await self.refresh()
await self.cog.save()
async def up(self, interaction: discord.Interaction, button: discord.ui.Button):
with suppress(discord.NotFound):
await interaction.response.defer()
page = self.pages[self.page]
self.selected -= 1
self.selected %= len(page.fields)
await self.refresh()
async def remove(self, interaction: discord.Interaction, button: discord.ui.Button):
command_name = self.get_command_name()
costs = self.get_costs()
if command_name not in costs:
return await interaction.response.send_message(_("Command not found!"), ephemeral=True, delete_after=10)
if self.global_bank:
del self.db.command_costs[command_name]
else:
conf = self.db.get_conf(self.ctx.guild)
del conf.command_costs[command_name]
await interaction.response.send_message(_("Command cost removed!"), ephemeral=True)
await self.refresh()
await self.cog.save()
# ROW 2
async def left(self, interaction: discord.Interaction, button: discord.ui.Button):
with suppress(discord.NotFound):
await interaction.response.defer()
self.page -= 1
self.page %= len(self.pages)
await self.refresh()
async def edit(self, interaction: discord.Interaction, button: discord.ui.Button):
cost_obj = self.get_cost_obj()
if not cost_obj:
return await interaction.response.send_message(_("Command not found!"), ephemeral=True)
data = {
"cost": str(cost_obj.cost),
"duration": str(cost_obj.duration),
"details": f"{cost_obj.level}, {cost_obj.prompt}, {cost_obj.modifier}",
"value": str(cost_obj.value),
}
title = _("Edit Cost: {}").format(self.get_command_name())
modal = CostModal(title, data)
await interaction.response.send_modal(modal)
await modal.wait()
if not modal.data:
return
cost_obj.cost = int(modal.data["cost"])
cost_obj.duration = int(modal.data["duration"])
cost_obj.level = modal.data["level"]
cost_obj.prompt = modal.data["prompt"]
cost_obj.modifier = modal.data["modifier"]
cost_obj.value = float(modal.data["value"])
if self.global_bank:
self.db.command_costs[self.get_command_name()] = cost_obj
else:
conf = self.db.get_conf(self.ctx.guild)
conf.command_costs[self.get_command_name()] = cost_obj
msg = await interaction.followup.send(_("Command cost updated!"), ephemeral=True)
if msg:
await msg.delete(delay=10)
await self.refresh()
await self.cog.save()
async def right(self, interaction: discord.Interaction, button: discord.ui.Button):
with suppress(discord.NotFound):
await interaction.response.defer()
self.page += 1
self.page %= len(self.pages)
await self.refresh()
# ROW 3
async def left10(self, interaction: discord.Interaction, button: discord.ui.Button):
with suppress(discord.NotFound):
await interaction.response.defer()
self.page -= 10
self.page %= len(self.pages)
await self.refresh()
async def down(self, interaction: discord.Interaction, button: discord.ui.Button):
with suppress(discord.NotFound):
await interaction.response.defer()
page = self.pages[self.page]
self.selected += 1
self.selected %= len(page.fields)
await self.refresh()
async def right10(self, interaction: discord.Interaction, button: discord.ui.Button):
with suppress(discord.NotFound):
await interaction.response.defer()
self.page += 10
self.page %= len(self.pages)
await self.refresh()
# ROW 4
async def close(self, interaction: discord.Interaction, button: discord.ui.Button):
with suppress(discord.NotFound):
await interaction.response.defer()
if msg := self.message:
with suppress(discord.NotFound):
await msg.delete()
self.stop()
await self.ctx.tick()