ux(rmenus): Add auto-swap where possible.
This commit is contained in:
@@ -209,6 +209,9 @@ class EconomyData(Registry, name='economy'):
|
|||||||
]
|
]
|
||||||
# Execute refund transactions
|
# Execute refund transactions
|
||||||
return await cls.execute_transactions(*records)
|
return await cls.execute_transactions(*records)
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class ShopTransaction(RowModel):
|
class ShopTransaction(RowModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from . import logger, babel
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .cog import RoleMenuCog
|
from .cog import RoleMenuCog
|
||||||
|
|
||||||
_p = babel._p
|
_p, _np = babel._p, babel._np
|
||||||
|
|
||||||
MISSING = object()
|
MISSING = object()
|
||||||
|
|
||||||
@@ -454,128 +454,49 @@ class RoleMenu:
|
|||||||
if emojikey(emoji) not in menu_emojis:
|
if emojikey(emoji) not in menu_emojis:
|
||||||
yield str(emoji)
|
yield str(emoji)
|
||||||
|
|
||||||
async def _handle_selection(self, lion, member: discord.Member, menuroleid: int):
|
async def _handle_positive(self, lion, member: discord.Member, mrole: RoleMenuRole) -> discord.Embed:
|
||||||
mrole = self.rolemap.get(menuroleid, None)
|
|
||||||
if mrole is None:
|
|
||||||
raise ValueError(f"Attempt to process event for invalid menuroleid {menuroleid}, THIS SHOULD NOT HAPPEN.")
|
|
||||||
|
|
||||||
guild = member.guild
|
|
||||||
|
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
|
guild = member.guild
|
||||||
role = guild.get_role(mrole.data.roleid)
|
role = guild.get_role(mrole.data.roleid)
|
||||||
if role is None:
|
if not role:
|
||||||
# This role no longer exists, nothing we can do
|
raise ValueError("Calling _handle_positive without a valid role.")
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'rolemenu|error:role_gone',
|
|
||||||
"This role no longer exists!"
|
|
||||||
))
|
|
||||||
)
|
|
||||||
if role in member.roles:
|
|
||||||
# Member already has the role, deselection case.
|
|
||||||
if self.config.sticky.value:
|
|
||||||
# Cannot deselect
|
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'rolemenu|deselect|error:sticky',
|
|
||||||
"**{role}** is a sticky role, you cannot remove it with this menu!"
|
|
||||||
)).format(role=role.name)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove the role
|
|
||||||
try:
|
|
||||||
await member.remove_roles(role)
|
|
||||||
except discord.Forbidden:
|
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'rolemenu|deselect|error:perms',
|
|
||||||
"I don't have enough permissions to remove this role from you!"
|
|
||||||
))
|
|
||||||
)
|
|
||||||
except discord.HTTPException:
|
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'rolemenu|deselect|error:discord',
|
|
||||||
"An unknown error occurred removing your role! Please try again later."
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update history
|
|
||||||
now = utc_now()
|
|
||||||
history = await self.cog.data.RoleMenuHistory.table.update_where(
|
|
||||||
menuid=self.data.menuid,
|
|
||||||
roleid=role.id,
|
|
||||||
userid=member.id,
|
|
||||||
removed_at=None,
|
|
||||||
).set(removed_at=now)
|
|
||||||
await self.cog.cancel_expiring_tasks(*(row['equipid'] for row in history))
|
|
||||||
|
|
||||||
# Refund if required
|
|
||||||
transactionids = [row['transactionid'] for row in history]
|
|
||||||
if self.config.refunds.value and any(transactionids):
|
|
||||||
transactionids = [tid for tid in transactionids if tid]
|
|
||||||
economy: Economy = self.bot.get_cog('Economy')
|
|
||||||
refunded = await economy.data.Transaction.refund_transactions(*transactionids)
|
|
||||||
total_refund = sum(row.amount + row.bonus for row in refunded)
|
|
||||||
else:
|
|
||||||
total_refund = 0
|
|
||||||
|
|
||||||
# Ack the removal
|
|
||||||
embed = discord.Embed(
|
|
||||||
colour=discord.Colour.brand_green(),
|
|
||||||
title=t(_p(
|
|
||||||
'rolemenu|deslect|success|title',
|
|
||||||
"Role removed"
|
|
||||||
))
|
|
||||||
)
|
|
||||||
if total_refund > 0:
|
|
||||||
embed.description = t(_p(
|
|
||||||
'rolemenu|deselect|success:refund|desc',
|
|
||||||
"You have removed **{role}**, and been refunded {coin} **{amount}**."
|
|
||||||
)).format(role=role.name, coin=self.bot.config.emojis.coin, amount=total_refund)
|
|
||||||
if total_refund < 0:
|
|
||||||
# TODO: Consider disallowing them from removing roles if their balance would go negative
|
|
||||||
embed.description = t(_p(
|
|
||||||
'rolemenu|deselect|success:negrefund|desc',
|
|
||||||
"You have removed **{role}**, and have lost {coin} **{amount}**."
|
|
||||||
)).format(role=role.name, coin=self.bot.config.emojis.coin, amount=-total_refund)
|
|
||||||
else:
|
|
||||||
embed.description = t(_p(
|
|
||||||
'rolemenu|deselect|success:norefund|desc',
|
|
||||||
"You have unequipped **{role}**."
|
|
||||||
)).format(role=role.name)
|
|
||||||
return embed
|
|
||||||
else:
|
|
||||||
# Member does not have the role, selection case.
|
|
||||||
required = self.config.required_role.data
|
|
||||||
if required is not None:
|
|
||||||
# Check member has the required role
|
|
||||||
if required not in [role.id for role in member.roles]:
|
|
||||||
role = guild.get_role(required)
|
|
||||||
name = role.name if role else required
|
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'rolemenu|select|error:required_role',
|
|
||||||
"You need to have the role **{role}** required to use this menu!"
|
|
||||||
)).format(role=name)
|
|
||||||
)
|
|
||||||
|
|
||||||
obtainable = self.config.obtainable.value
|
|
||||||
if obtainable is not None:
|
|
||||||
# Check shared roles
|
|
||||||
menu_roleids = {mrole.data.roleid for mrole in self.roles}
|
|
||||||
member_roleids = {role.id for role in member.roles}
|
|
||||||
common = len(menu_roleids.intersection(member_roleids))
|
|
||||||
if common >= obtainable:
|
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'rolemenu|select|error:max_obtainable',
|
|
||||||
"You already have the maximum of {obtainable} roles from this menu!"
|
|
||||||
)).format(obtainable=obtainable)
|
|
||||||
)
|
|
||||||
|
|
||||||
price = mrole.config.price.value
|
price = mrole.config.price.value
|
||||||
|
|
||||||
|
obtainable = self.config.obtainable.value
|
||||||
|
remove_line = ''
|
||||||
|
if obtainable is not None:
|
||||||
|
# Check shared roles
|
||||||
|
menu_roles = {mrole.data.roleid: mrole for mrole in self.roles}
|
||||||
|
common = [role for role in member.roles if role.id in menu_roles]
|
||||||
|
|
||||||
|
if len(common) >= obtainable:
|
||||||
|
swap = None
|
||||||
|
if len(common) == 1 and not self.config.sticky.value:
|
||||||
|
swap = menu_roles[common[0].id]
|
||||||
|
# Check if LC will be lost by exchanging the role
|
||||||
|
if (swap.config.price.value) > 0 and not self.config.refunds.value:
|
||||||
|
swap = None
|
||||||
|
if swap is not None:
|
||||||
|
# Do remove
|
||||||
|
try:
|
||||||
|
remove_embed = await self._handle_negative(lion, member, swap)
|
||||||
|
remove_line = remove_embed.description
|
||||||
|
except UserInputError:
|
||||||
|
# If we failed to remove for some reason, pretend we didn't try
|
||||||
|
swap = None
|
||||||
|
|
||||||
|
if swap is None:
|
||||||
|
error = t(_np(
|
||||||
|
'rolemenu|select|error:max_obtainable',
|
||||||
|
"You can own at most one role from this menu! You currently own:",
|
||||||
|
"You can own at most **{count}** roles from this menu! You currently own:",
|
||||||
|
obtainable
|
||||||
|
)).format(count=obtainable)
|
||||||
|
error = '\n'.join((error, *(role.mention for role in common)))
|
||||||
|
raise UserInputError(error)
|
||||||
|
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
# Check member balance
|
# Check member balance
|
||||||
# TODO: More transaction safe (or rather check again after transaction)
|
# TODO: More transaction safe (or rather check again after transaction)
|
||||||
@@ -661,7 +582,7 @@ class RoleMenu:
|
|||||||
else:
|
else:
|
||||||
embed.description = t(_p(
|
embed.description = t(_p(
|
||||||
'rolemenu|select|success:nopurchase|desc',
|
'rolemenu|select|success:nopurchase|desc',
|
||||||
"You have equipped the role **{role}**"
|
"You have equipped **{role}**"
|
||||||
)).format(role=role.name)
|
)).format(role=role.name)
|
||||||
|
|
||||||
if expiry is not None:
|
if expiry is not None:
|
||||||
@@ -671,9 +592,134 @@ class RoleMenu:
|
|||||||
)).format(
|
)).format(
|
||||||
timestamp=discord.utils.format_dt(expiry)
|
timestamp=discord.utils.format_dt(expiry)
|
||||||
)
|
)
|
||||||
|
if remove_line:
|
||||||
|
embed.description = '\n'.join((remove_line, embed.description))
|
||||||
|
|
||||||
# TODO Event logging
|
# TODO Event logging
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
|
async def _handle_negative(self, lion, member: discord.Member, mrole: RoleMenuRole) -> discord.Embed:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
guild = member.guild
|
||||||
|
role = guild.get_role(mrole.data.roleid)
|
||||||
|
if not role:
|
||||||
|
raise ValueError("Calling _handle_negative without a valid role.")
|
||||||
|
|
||||||
|
if self.config.sticky.value:
|
||||||
|
# Cannot deselect
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'rolemenu|deselect|error:sticky',
|
||||||
|
"**{role}** is a sticky role, you cannot remove it with this menu!"
|
||||||
|
)).format(role=role.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove the role
|
||||||
|
try:
|
||||||
|
await member.remove_roles(role)
|
||||||
|
except discord.Forbidden:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'rolemenu|deselect|error:perms',
|
||||||
|
"I don't have enough permissions to remove this role from you!"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'rolemenu|deselect|error:discord',
|
||||||
|
"An unknown error occurred removing your role! Please try again later."
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update history
|
||||||
|
now = utc_now()
|
||||||
|
history = await self.cog.data.RoleMenuHistory.table.update_where(
|
||||||
|
menuid=self.data.menuid,
|
||||||
|
roleid=role.id,
|
||||||
|
userid=member.id,
|
||||||
|
removed_at=None,
|
||||||
|
).set(removed_at=now)
|
||||||
|
await self.cog.cancel_expiring_tasks(*(row['equipid'] for row in history))
|
||||||
|
|
||||||
|
# Refund if required
|
||||||
|
transactionids = [row['transactionid'] for row in history]
|
||||||
|
if self.config.refunds.value and any(transactionids):
|
||||||
|
transactionids = [tid for tid in transactionids if tid]
|
||||||
|
economy: Economy = self.bot.get_cog('Economy')
|
||||||
|
refunded = await economy.data.Transaction.refund_transactions(*transactionids)
|
||||||
|
total_refund = sum(row.amount + row.bonus for row in refunded)
|
||||||
|
else:
|
||||||
|
total_refund = 0
|
||||||
|
|
||||||
|
# Ack the removal
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
title=t(_p(
|
||||||
|
'rolemenu|deslect|success|title',
|
||||||
|
"Role removed"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
if total_refund > 0:
|
||||||
|
embed.description = t(_p(
|
||||||
|
'rolemenu|deselect|success:refund|desc',
|
||||||
|
"You have removed **{role}**, and been refunded {coin} **{amount}**."
|
||||||
|
)).format(role=role.name, coin=self.bot.config.emojis.coin, amount=total_refund)
|
||||||
|
if total_refund < 0:
|
||||||
|
# TODO: Consider disallowing them from removing roles if their balance would go negative
|
||||||
|
embed.description = t(_p(
|
||||||
|
'rolemenu|deselect|success:negrefund|desc',
|
||||||
|
"You have removed **{role}**, and have lost {coin} **{amount}**."
|
||||||
|
)).format(role=role.name, coin=self.bot.config.emojis.coin, amount=-total_refund)
|
||||||
|
else:
|
||||||
|
embed.description = t(_p(
|
||||||
|
'rolemenu|deselect|success:norefund|desc',
|
||||||
|
"You have unequipped **{role}**."
|
||||||
|
)).format(role=role.name)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
async def _handle_selection(self, lion, member: discord.Member, menuroleid: int):
|
||||||
|
lock_key = ('rmenu', member.id, member.guild.id)
|
||||||
|
async with self.bot.idlock(lock_key):
|
||||||
|
# TODO: Selection locking
|
||||||
|
mrole = self.rolemap.get(menuroleid, None)
|
||||||
|
if mrole is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Attempt to process event for invalid menuroleid {menuroleid}, THIS SHOULD NOT HAPPEN."
|
||||||
|
)
|
||||||
|
|
||||||
|
guild = member.guild
|
||||||
|
t = self.bot.translator.t
|
||||||
|
role = guild.get_role(mrole.data.roleid)
|
||||||
|
if role is None:
|
||||||
|
# This role no longer exists, nothing we can do
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'rolemenu|error:role_gone',
|
||||||
|
"The role **{name}** no longer exists!"
|
||||||
|
)).format(name=mrole.data.label)
|
||||||
|
)
|
||||||
|
|
||||||
|
required = self.config.required_role.data
|
||||||
|
if required is not None:
|
||||||
|
# Check member has the required role
|
||||||
|
if required not in [role.id for role in member.roles]:
|
||||||
|
role = guild.get_role(required)
|
||||||
|
name = role.name if role else required
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'rolemenu|select|error:required_role',
|
||||||
|
"You need to have the role **{role}** required to use this menu!"
|
||||||
|
)).format(role=name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if role in member.roles:
|
||||||
|
# Member already has the role, deselection case.
|
||||||
|
return await self._handle_negative(lion, member, mrole)
|
||||||
|
else:
|
||||||
|
# Member does not have the role, selection case.
|
||||||
|
return await self._handle_positive(lion, member, mrole)
|
||||||
|
|
||||||
async def interactive_selection(self, interaction: discord.Interaction, menuroleid: int):
|
async def interactive_selection(self, interaction: discord.Interaction, menuroleid: int):
|
||||||
"""
|
"""
|
||||||
Handle a component interaction callback for this menu.
|
Handle a component interaction callback for this menu.
|
||||||
|
|||||||
Reference in New Issue
Block a user