Files
croccybot/bot/modules/shop/shops/colours.py

1181 lines
42 KiB
Python

from typing import TYPE_CHECKING, Optional
import logging
import asyncio
import discord
from discord.ext import commands as cmds
from discord import app_commands as appcmds
from discord.ui.select import select, Select, SelectOption
from discord.ui.button import button, Button
from meta import LionCog, LionContext, LionBot
from meta.errors import SafeCancellation
from utils import ui
from utils.lib import error_embed
from constants import MAX_COINS
from .. import babel
from ..data import ShopData, ShopItemType
from .base import ShopCog, Shop, Customer, Store, ShopItem
if TYPE_CHECKING:
from ..cog import Shopping
from modules.economy.cog import Economy
_p = babel._p
logger = logging.getLogger(__name__)
class ColourRoleItem(ShopItem):
"""
ShopItem representing an equippable Colour Role.
"""
@property
def role(self) -> Optional[discord.Role]:
"""
Retrieves the discord Role corresponding to this colour role,
if it exists.
"""
guild = self.bot.get_guild(self.data.guildid)
if guild is not None:
return guild.get_role(self.data.roleid)
@property
def name(self):
"""
An appropriate name for this Colour Role.
Tries to use the role name if it is available.
If it doesn't exist, instead uses the saved role id.
"""
if (role := self.role) is not None:
return role.name
else:
return str(self.data.roleid)
@property
def mention(self):
"""
Helper method to mention the contained role.
Avoids using the role object, in case it is not cached
or no longer exists.
"""
return f"<@&{self.data.roleid}>"
@property
def price(self):
"""
The price of the item.
"""
return self.data.price
@property
def itemid(self):
"""
The global itemid for this item.
"""
return self.data.itemid
@property
def colour(self) -> Optional[discord.Colour]:
"""
Returns the colour of the linked role.
If the linked role does not exist or cannot be found,
returns None.
"""
if (role := self.role) is not None:
return role.colour
else:
return None
def select_option_for(self, customer: Customer, owned=False) -> SelectOption:
t = customer.bot.translator.t
value = str(self.itemid)
if not owned:
label = t(_p(
'ui:colourstore|menu:buycolours|label',
"{name} ({price} LC)"
)).format(name=self.name, price=self.price)
else:
label = t(_p(
'ui:colourstore|menu:buycolours|label',
"{name} (This is your colour!)"
)).format(name=self.name)
if (colour := self.colour) is not None:
description = t(_p(
'ui:colourstore|menu:buycolours|desc',
"Colour: {colour}"
)).format(colour=str(colour))
else:
description = t(_p(
'ui:colourstore|menu:buycolours|desc',
"Colour: Unknown"
))
return SelectOption(
label=label,
value=value,
description=description,
default=owned
)
class ColourShop(Shop):
"""
A Shop class representing a Colour shop for a given customer.
"""
_name_ = _p("shop:colours|name", 'Colour Shop')
_item_type_ = ShopItemType.COLOUR
def purchasable(self):
"""
Returns a list of ColourRoleItems
that the customer can afford, and does not own.
"""
owned = self.owned()
balance = self.customer.balance
return [
item for item in self.items
if (owned is None or item.itemid != owned.itemid) and (item.price <= balance)
]
async def purchase(self, itemid) -> ColourRoleItem:
"""
Atomically handle a purchase of a ColourRoleItem.
Various errors may occur here, from the user not actually having enough funds,
to the ColourRole not being purchasable (because of e.g. permissions),
or even the `itemid` somehow not referring to a colour role in the correct guild.
If the purchase completes successfully, returns the purchased ColourRoleItem.
If the purchase fails for a known reason, raises SafeCancellation, with the error information.
"""
t = self.bot.translator.t
conn = await self.bot.db.get_connection()
async with conn.transaction():
# Retrieve the item to purchase from data
item = await self.data.ShopItemInfo.table.select_one_where(itemid=itemid)
# Ensure the item is purchasable and not deleted
if not item['purchasable'] or item['deleted']:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:not_purchasable',
"This item may not be purchased!"
))
)
# Refresh the customer
await self.customer.refresh()
# Ensure the guild exists in cache
guild = self.bot.get_guild(self.customer.guildid)
if guild is None:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:no_guild',
"Could not retrieve the server from Discord!"
))
)
# Ensure the customer member actually exists
member = await self.customer.lion.get_member()
if member is None:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:no_member',
"Could not retrieve the member from Discord."
))
)
# Ensure the purchased role actually exists
role = guild.get_role(item['roleid'])
if role is None:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:no_role',
"This colour role could not be found in the server."
))
)
# Ensure the customer has enough coins for the item
if self.customer.balance < item['price']:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:low_balance',
"This item costs {coin}{amount}!\nYour balance is {coin}{balance}"
)).format(
coin=self.bot.config.emojis.getemoji('coin'),
amount=item['price'],
balance=self.customer.balance
)
)
owned = self.owned()
if owned is not None:
# Ensure the customer does not already own the item
if owned.itemid == item['itemid']:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:owned',
"You already own this item!"
))
)
# Charge the customer for the item
economy_cog: Economy = self.bot.get_cog('Economy')
economy_data = economy_cog.data
transaction = await economy_data.ShopTransaction.purchase_transaction(
guild.id,
member.id,
member.id,
itemid,
item['price']
)
# Add the item to the customer's inventory
await self.data.MemberInventory.create(
guildid=guild.id,
userid=member.id,
transactionid=transaction.transactionid,
itemid=itemid
)
# Give the customer the role (do rollback if this fails)
try:
await member.add_roles(
role,
atomic=True,
reason="Purchased colour role"
)
except discord.NotFound:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:failed_no_role',
"This colour role no longer exists!"
))
)
except discord.Forbidden:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:failed_permissions',
"I do not have enough permissions to give you this colour role!"
))
)
except discord.HTTPException:
raise SafeCancellation(
t(_p(
'shop:colour|purchase|error:failed_unknown',
"An unknown error occurred while giving you this colour role!"
))
)
# At this point, the purchase has succeeded and the user has obtained the colour role
# Now, remove their previous colour role (if applicable)
# TODO: We should probably add an on_role_delete event to clear defunct colour roles
if owned is not None:
owned_role = owned.role
if owned_role is not None:
try:
await member.remove_roles(
owned_role,
reason="Removing old colour role.",
atomic=True
)
except discord.HTTPException:
# Possibly Forbidden, or the role doesn't actually exist anymore (cache failure)
pass
await self.data.MemberInventory.table.delete_where(inventoryid=owned.data.inventoryid)
# Purchase complete, update the shop and customer
await self.refresh()
return self.owned()
async def refresh(self):
"""
Refresh the customer and item data.
"""
data = await self.data.ShopItemInfo.table.select_where(
item_type=self._item_type_,
deleted=False,
guildid=self.customer.guildid
).order_by('itemid')
self.items = [ColourRoleItem(self.bot, self.data.ShopItemInfo(row)) for row in data]
await self.customer.refresh()
def owned(self) -> Optional[ColourRoleItem]:
"""
Returns the ColourRoleItem currently owned by the Customer, if any.
Since this item may have been deleted, it may not appear in the shop inventory!
"""
for item in self.customer.inventory:
if item.item_type is self._item_type_:
return ColourRoleItem(self.bot, item)
def make_store(self, interaction: discord.Interaction):
return ColourStore(self, interaction)
@ShopCog.register
class ColourShopping(ShopCog):
"""
Cog in charge of colour shopping.
Registers colour shop related commands and methods.
"""
_shop_cls_ = ColourShop
async def load_into(self, cog: 'Shopping'):
self.crossload_group(self.editshop_group, cog.editshop_group)
await cog.bot.add_cog(self)
@LionCog.placeholder_group
@cmds.hybrid_group('editshop', with_app_command=False)
async def editshop_group(self, ctx: LionContext):
pass
@editshop_group.group(_p('grp:editshop_colours', 'colours'))
async def editshop_colours_group(self, ctx: LionContext):
pass
@editshop_colours_group.command(
name=_p('cmd:editshop_colours_create', 'create'),
description=_p(
'cmd:editshop_colours_create|desc',
"Create a new colour role with the given colour."
)
)
@appcmds.rename(
colour=_p('cmd:editshop_colours_create|param:colour', "colour"),
name=_p('cmd:editshop_colours_create|param:name', "name"),
price=_p('cmd:editshop_colours_create|param:price', "price")
)
@appcmds.describe(
colour=_p(
'cmd:editshop_colours_create|param:colour|desc',
"What colour should the role be? (As a hex code, e.g. #AB22AB)"
),
name=_p(
'cmd:editshop_colours_create|param:name|desc',
"What should the colour role be called?"
),
price=_p(
'cmd:editshop_colours_create|param:price|desc',
"How much should the colour role cost?"
)
)
async def editshop_colours_create_cmd(self, ctx: LionContext,
colour: appcmds.Range[str, 3, 100],
name: appcmds.Range[str, 1, 100],
price: appcmds.Range[int, 0, MAX_COINS]):
"""
Create a new colour role with the specified attributes.
"""
t = self.bot.translator.t
if not ctx.interaction:
return
if not ctx.guild:
return
try:
actual_colour = discord.Colour.from_str(colour)
except ValueError:
raise SafeCancellation(
t(_p(
'cmd:editshop_colours_create|error:parse_colour',
"I could not extract a colour value from `{colour}`!\n"
"Please enter the colour as a hex string, e.g. `#FA0BC1`"
)).format(colour=colour)
)
# Check we actually have permissions to create the role
if not ctx.guild.me.guild_permissions.manage_roles:
raise SafeCancellation(
t(_p(
'cmd:editshop_colours_create|error:perms',
"I do not have permission to create server roles!\n"
"Please either give me this permission, "
"or create the role manually and use `/editshop colours add` instead."
))
)
# Check our current colour roles, make sure we don't have 25 already
current = await self.data.ShopItemInfo.fetch_where(
guildid=ctx.guild.id,
item_type=self._shop_cls_._item_type_,
deleted=False
)
if len(current) >= 25:
raise SafeCancellation(
t(_p(
'cmd:editshop_colours_create|error:max_colours',
"This server already has the maximum of `25` colour roles!\n"
"Please remove some before adding or creating more."
))
)
# Create the role
try:
role = await ctx.guild.create_role(
name=name,
colour=actual_colour,
hoist=False,
mentionable=False,
reason="Creating Colour Role (/editshop colours create)"
)
except discord.HTTPException:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:editshop_colours_create|error:failed_unknown',
"An unknown Discord error occurred while creating your colour role!\n"
"Please try again in a few minutes."
))
),
ephemeral=True
)
await logger.warning(
"Unexpected Discord exception occurred while creating a colour role.",
exc_info=True
)
return
# Identify where we should put the role
# If the server already has colour roles, we put it underneath those,
# as long as that is below our own top role.
# Otherwise, we leave them alone
current_roles = (ctx.guild.get_role(item.roleid) for item in current)
current_roles = [role for role in current_roles if role is not None]
if current_roles:
position = min(*current_roles, ctx.guild.me.top_role).position
position -= 1
else:
position = 0
if position > 0:
# Due to the imprecise nature of Discord role ordering, this may fail.
try:
role = await role.edit(position=position)
except discord.Forbidden:
position = 0
# Now that the role is set up, add it to data
item = await self.data.ShopItem.create(
guildid=ctx.guild.id,
item_type=self._shop_cls_._item_type_,
price=price,
purchasable=True
)
await self.data.ColourRole.create(
itemid=item.itemid,
roleid=role.id
)
# And finally ack the request
embed = discord.Embed(
colour=actual_colour,
)
embed.title = t(_p(
'cmd:editshop_colours_create|resp:done|title',
"Colour Role Created"
))
embed.description = t(_p(
'cmd:editshop_colours_create|resp:done|desc',
"You have created the role {mention}, "
"and added it to the colour shop for {coin}**{price}**!"
)).format(mention=role.mention, coin=self.bot.config.emojis.getemoji('coin'), price=price)
if position == 0:
note = t(_p(
'cmd:editshop_colours_create|resp:done|field:position_note|value',
"The new colour role was added below all other roles. "
"Remember a member's active colour is determined by their highest coloured role!"
))
embed.add_field(
name=t(_p('cmd:editshop_colours_create|resp:done|field:position_note|name', "Note")),
value=note
)
await ctx.reply(
embed=embed,
)
@editshop_colours_group.command(
name=_p('cmd:editshop_colours_edit', 'edit'),
description=_p(
'cmd:editshop_colours_edit|desc',
"Edit the name, colour, or price of a colour role."
)
)
@appcmds.rename(
role=_p('cmd:editshop_colours_edit|param:role', "role"),
name=_p('cmd:editshop_colours_edit|param:name', "name"),
colour=_p('cmd:editshop_colours_edit|param:colour', "colour"),
price=_p('cmd:editshop_colours_edit|param:price', "price"),
)
@appcmds.describe(
role=_p(
'cmd:editshop_colours_edit|param:role|desc',
"Select a colour role to edit."
),
name=_p(
'cmd:editshop_colours_edit|param:name|desc',
"New name to give the colour role."
),
colour=_p(
'cmd:editshop_colours_edit|param:colour|desc',
"New colour for the colour role (as hex, e.g. #AB12AB)."
),
price=_p(
'cmd:editshop_colours_edit|param:price|desc',
"New price for the colour role."
),
)
async def editshop_colours_edit_cmd(self, ctx: LionContext,
role: discord.Role,
name: Optional[appcmds.Range[str, 1, 100]],
colour: Optional[appcmds.Range[str, 3, 100]],
price: Optional[appcmds.Range[int, 0, MAX_COINS]]):
"""
Edit the provided colour role with the given attributes.
"""
t = self.bot.translator.t
if not ctx.guild:
return
if not ctx.interaction:
return
# First check the provided role is actually a colour role
items = await self.data.ShopItemInfo.fetch_where(
guildid=ctx.guild.id,
deleted=False,
item_type=self._shop_cls_._item_type_,
roleid=role.id
)
if not items:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:editshop_colours_edit|error:invalid_role',
"{mention} is not in the colour role shop!"
)).format(mention=role.mention)
),
ephemeral=True
)
return
item = items[0]
# Check that we actually have something to edit
if name is None and colour is None and price is None:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:editshop_colours_edit|error:no_args',
"You must give me one of `name`, `colour`, or `price` to update!"
))
),
ephemeral=True
)
return
# Check the colour works
if colour is not None:
try:
actual_colour = discord.Colour.from_str(colour)
except ValueError:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:editshop_colours_edit|error:parse_colour',
"I could not extract a colour value from `{colour}`!\n"
"Please enter the colour as a hex string, e.g. `#FA0BC1`"
)).format(colour=colour)
),
ephemeral=True
)
return
if name is not None or colour is not None:
# Check we actually have permissions to update the role if needed
if not ctx.guild.me.guild_permissions.manage_roles or not ctx.guild.me.top_role > role:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:editshop_colours_edit|error:perms',
"I do not have sufficient server permissions to edit this role!"
))
),
ephemeral=True
)
return
# Now update the information
lines = []
if price is not None:
await self.data.ShopItem.table.update_where(
itemid=item.itemid
).set(price=price)
lines.append(
t(_p(
'cmd:editshop_colours_edit|resp:done|line:price',
"{tick} Set price to {coin}**{price}**"
)).format(
tick=self.bot.config.emojis.getemoji('tick'),
coin=self.bot.config.emojis.getemoji('coin'),
price=price
)
)
if name is not None or colour is not None:
args = {}
if name is not None:
args['name'] = name
if colour is not None:
args['colour'] = actual_colour
role = await role.edit(**args)
lines.append(
t(_p(
'cmd:editshop_colours_edit|resp:done|line:role',
"{tick} Updated role to {mention}"
)).format(
tick=self.bot.config.emojis.getemoji('tick'),
mention=role.mention
)
)
description = '\n'.join(lines)
await ctx.reply(
embed=discord.Embed(
title=t(_p('cmd:editshop_colours_edit|resp:done|embed:title', "Colour Role Updated")),
description=description
)
)
@editshop_colours_group.command(
name=_p('cmd:editshop_colours_auto', 'auto'),
description=_p('cmd:editshop_colours_auto|desc', "Automatically create a set of colour roles.")
)
async def editshop_colours_auto_cmd(self, ctx: LionContext):
"""
Automatically create a set of colour roles.
"""
await ctx.reply("Not Implemented Yet")
@editshop_colours_group.command(
name=_p('cmd:editshop_colours_add', 'add'),
description=_p(
'cmd:editshop_colours_add|desc',
"Add an existing role to the colour shop."
)
)
@appcmds.rename(
role=_p('cmd:editshop_colours_add|param:role', "role"),
price=_p('cmd:editshop_colours_add|param:price', "price")
)
@appcmds.describe(
role=_p(
'cmd:editshop_colours_add|param:role|desc',
"Select a role to add to the colour shop."
),
price=_p(
'cmd:editshop_colours_add|param:price|desc',
"How much should this role cost?"
)
)
async def editshop_colours_add_cmd(self, ctx: LionContext,
role: discord.Role,
price: appcmds.Range[int, 0, MAX_COINS]):
"""
Add a new colour role from an existing role.
"""
t = self.bot.translator.t
if not ctx.interaction:
return
if not ctx.guild:
return
# Check our current colour roles, make sure we don't have 25 already
current = await self.data.ShopItemInfo.fetch_where(
guildid=ctx.guild.id,
item_type=self._shop_cls_._item_type_,
deleted=False
)
if len(current) >= 25:
raise SafeCancellation(
t(_p(
'cmd:editshop_colours_add|error:max_colours',
"This server already has the maximum of `25` colour roles!\n"
"Please remove some before adding or creating more."
))
)
# Also check the role isn't currently in the role list
if role.id in [item.roleid for item in current]:
raise SafeCancellation(
t(_p(
'cmd:editshop_colours_add|error:role_exists',
"The role {mention} is already registered as a colour role!"
)).format(mention=role.mention)
)
# Add the role to data
item = await self.data.ShopItem.create(
guildid=ctx.guild.id,
item_type=self._shop_cls_._item_type_,
price=price,
purchasable=True
)
await self.data.ColourRole.create(
itemid=item.itemid,
roleid=role.id
)
# And finally ack the request
embed = discord.Embed(
colour=role.colour,
)
embed.title = t(_p('cmd:editshop_colours_add|resp:done|embed:title', "Colour Role Created"))
embed.description = t(_p(
'cmd:editshop_colours_add|resp:done|embed:desc',
"You have added {mention} to the colour shop for {coin}**{price}**!"
)).format(mention=role.mention, coin=self.bot.config.emojis.getemoji('coin'), price=price)
await ctx.reply(
embed=embed,
)
@editshop_colours_group.command(
name=_p('cmd:editshop_colours_clear', 'clear'),
description=_p(
'cmd:editshop_colours_clear|desc',
"Remove all the colour roles from the shop, and optionally delete the roles."
)
)
@appcmds.rename(
delete=_p('cmd:editshop_colours_clear|param:delete', "delete_roles")
)
@appcmds.rename(
delete=_p(
'cmd:editshop_colours_clear|param:delete|desc',
"Also delete the associated roles."
)
)
async def editshop_colours_clear_cmd(self, ctx: LionContext, delete: Optional[bool]):
"""
Remove all of the colour roles.
Optionally refund and/or delete the roles themselves.
"""
t = self.bot.translator.t
if not ctx.guild:
return
if not ctx.interaction:
return
# TODO: Implement refund
refund = False
# Fetch our current colour roles
current = await self.data.ShopItemInfo.fetch_where(
guildid=ctx.guild.id,
item_type=self._shop_cls_._item_type_,
deleted=False
)
itemids = [item.itemid for item in current]
roles = (ctx.guild.get_role(item.roleid) for item in current)
roles = [role for role in roles if role is not None]
if refund:
inventory_items = await self.data.MemberInventory.fetch_where(
guildid=ctx.guild.id,
itemid=itemids
)
else:
inventory_items = None
# If we don't have any, error out gracefully
if not current:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:editshop_colours_clear|error:no_colours',
"There are no coloured roles to remove!"
))
),
ephemeral=True
)
return
deleted = []
delete_failed = []
refunded = []
async def delete_roles():
for role in roles:
try:
await role.delete(reason="Clearing colour role shop")
deleted.append(role)
except discord.HTTPException:
delete_failed.append(role)
await asyncio.sleep(0.2)
async def refund_members():
"""
Refunds the members with the colour roles.
"""
...
async def status_loop():
while True:
try:
done = await status_update()
if done:
break
else:
await asyncio.sleep(2)
except asyncio.CancelledError:
return
async def status_update():
tick = self.bot.config.emojis.getemoji('tick')
loading = self.bot.config.emojis.getemoji('loading')
lines = []
cleared_line = t(_p(
'cmd:editshop_colours_clear|resp:done|line:clear',
"{tick} Colour shop cleared."
)).format(tick=tick)
lines.append(cleared_line)
done = True
if refund:
count = len(refunded)
total = len(inventory_items)
if count < total:
refund_line = t(_p(
'cmd:editshop_colours_clear|resp:done|line:refunding',
"{loading} Refunded **{count}/{total}** members."
))
done = False
else:
refund_line = t(_p(
'cmd:editshop_colours_clear|resp:done|line:refunded',
"{tick} Refunded **{total}/{total}** members."
))
lines.append(
refund_line.format(tick=tick, loading=loading, count=count, total=total)
)
if delete:
count = len(deleted)
failed = len(delete_failed)
total = len(roles)
if failed:
delete_line = t(_p(
'cmd:editshop_colours_clear|resp:done|line:deleted_failed',
"{emoji} Deleted **{count}/{total}** colour roles. (**{failed}** failed!)"
))
else:
delete_line = t(_p(
'cmd:editshop_colours_clear|resp:done|line:deleted',
"{emoji} Deleted **{count}/{total}** colour roles."
))
if count + failed < total:
done = False
lines.append(
delete_line.format(
emoji=loading if count + failed < total else tick,
count=count, total=total, failed=failed
)
)
description = '\n'.join(lines)
embed = discord.Embed(
colour=discord.Colour.light_grey() if not done else discord.Colour.brand_green(),
description=description
)
await ctx.interaction.edit_original_response(embed=embed)
return done
await ctx.interaction.response.defer(thinking=True)
# Run the clear
await self.data.ShopItem.table.update_where(itemid=itemids).set(deleted=True)
tasks = []
# Refund the members (slowly)
if refund:
tasks.append(asyncio.create_task(refund_members()))
# Delete the roles (slowly)
if delete:
tasks.append(asyncio.create_task(delete_roles()))
loop_task = None
try:
if tasks:
loop_task = asyncio.create_task(status_loop())
tasks.append(loop_task)
await asyncio.gather(*tasks)
else:
await status_update()
finally:
if loop_task is not None and not loop_task.done() and not loop_task.cancelled():
loop_task.cancel()
await status_update()
@editshop_colours_group.command(
name=_p('cmd:editshop_colours_remove', 'remove'),
description=_p(
'cmd:editshop_colours_remove|desc',
"Remove a specific colour role from the shop."
)
)
@appcmds.rename(
role=_p('cmd:editshop_colours_remove|param:role', "role"),
delete_role=_p('cmd:editshop_colours_remove', "delete_role")
)
@appcmds.describe(
role=_p(
'cmd:editshop_colours_remove|param:role|desc',
"Select the colour role to remove."
),
delete_role=_p(
'cmd:editshop_colours_remove|param:delete_role|desc',
"Whether to delete the associated role."
)
)
async def editshop_colours_remove_cmd(self, ctx: LionContext, role: discord.Role, delete_role: Optional[bool]):
"""
Remove a specific colour role.
"""
t = self.bot.translator.t
if not ctx.guild:
return
if not ctx.interaction:
return
# First check the provided role is actually a colour role
items = await self.data.ShopItemInfo.fetch_where(
guildid=ctx.guild.id,
deleted=False,
item_type=self._shop_cls_._item_type_,
roleid=role.id
)
if not items:
await ctx.reply(
embed=error_embed(
t(_p(
'cmd:editshop_colours_remove|error:not_colour',
"{mention} is not in the colour role shop!"
)).format(mention=role.mention)
),
ephemeral=True
)
return
item = items[0]
# Delete the item, respecting the delete setting.
await self.data.ShopItem.table.update_where(itemid=item.itemid, deleted=True)
if delete_role:
role = ctx.guild.get_role(item.roleid)
if role:
try:
await role.delete()
role_msg = t(_p(
'cmd:editshop_colours_remove|resp:done|line:delete',
"Successfully deleted the role."
))
except discord.Forbidden:
role_msg = t(_p(
'cmd:editshop_colours_remove|resp:done|line:delete',
"I do not have sufficient permissions to delete the role."
))
except discord.HTTPException:
role_msg = t(_p(
'cmd:editshop_colours_remove|resp:done|line:delete',
"Failed to delete the role for an unknown reason."
))
else:
role_msg = t(_p(
'cmd:editshop_colours_remove|resp:done|line:delete',
"Could not find the role in order to delete it."
))
else:
role_msg = ""
# Ack the action
await ctx.reply(
embed=discord.Embed(
colour=discord.Colour.brand_green(),
description=t(_p(
'cmd:editshop_colours_remove|resp:done|embed:desc',
"Removed {mention} from the colour shop.\n{delete_line}"
)).format(mention=role.mention, delete_line=role_msg)
)
)
async def editshop_colours_remove_acmpl_item(self, interaction: discord.Interaction, partial: str):
"""
This is not currently in use.
Intended to be transferred to `/shop buy` autocomplete.
"""
items = await self.data.ShopItemInfo.fetch_where(
guildid=interaction.guild.id,
deleted=False,
item_type=self._shop_cls_._item_type_
).order_by('itemid')
if not items:
return [
appcmds.Choice(
name="The colour role shop is empty!",
value=partial
)
]
else:
options = [
(str(i), "[{itemid:02}] | {price} LC | {colour} | @{name}".format(
itemid=i,
price=item.price,
colour=role.colour if (role := interaction.guild.get_role(item.roleid)) is not None else "#??????",
name=role.name if role is not None else "deleted-role"
))
for i, item in enumerate(items, start=1)
]
options = [option for option in options if partial.lower() in option[1].lower()]
return [appcmds.Choice(name=option[1], value=option[0]) for option in options]
class ColourStore(Store):
"""
Ephemeral UI providing access to the colour store.
"""
shop: ColourShop
@select(placeholder=_p("ui:colourstore|menu:buycolours|placeholder", "Select to Buy"))
async def select_colour(self, interaction: discord.Interaction, selection: Select):
t = self.bot.translator.t
# User selected a colour from the list
# Run purchase pathway for that item
# The selection value should be the global itemid
# However, if the selection is currently owned, do absolutely nothing.
itemid = int(selection.values[0])
if (owned := self.shop.owned()) and owned.itemid == itemid:
await interaction.response.defer()
else:
# Run purchase pathway
await interaction.response.defer(ephemeral=True, thinking=True)
try:
item = await self.shop.purchase(itemid)
except SafeCancellation as exc:
embed = discord.Embed(
title=t(_p('ui:colourstore|menu:buycolours|embed:error|title', "Purchase Failed!")),
colour=discord.Colour.brand_red(),
description=exc.msg
)
await interaction.edit_original_response(embed=embed)
else:
# Ack purchase
embed = discord.Embed(
colour=discord.Colour.brand_green(),
description=t(_p(
'ui:colourstore|menu:buycolours|resp:done|desc',
"{tick} You have purchased {mention}"
)).format(
mention=item.mention,
tick=self.shop.bot.config.emojis.getemoji('tick')
)
)
await interaction.edit_original_response(embed=embed)
await self.refresh()
await self.redraw()
async def select_colour_refresh(self):
"""
Refresh the select colour menu.
For an item to be purchasable,
it needs to be affordable and not currently owned by the member.
"""
t = self.bot.translator.t
selector = self.select_colour
# Get the list of ColourRoleItems that may be purchased
purchasable = self.shop.purchasable()
owned = self.shop.owned()
option_map: dict[int, SelectOption] = {}
for item in purchasable:
option_map[item.itemid] = item.select_option_for(self.shop.customer)
if owned is not None and owned.role is not None:
option_map[owned.itemid] = owned.select_option_for(self.shop.customer, owned=True)
if not option_map:
selector.placeholder = t(_p(
'ui:colourstore|menu:buycolours|placeholder',
"There are no colour roles available to purchase!"
))
selector.disabled = True
else:
selector.placeholder = t(_p(
'ui:colourstore|menu:buycolours|placeholder',
"Select a colour role to purchase!"
))
selector.disabled = False
selector.options = list(option_map.values())
async def refresh(self):
"""
Refresh the UI elements
"""
await self.select_colour_refresh()
if not self.select_colour.options:
self._layout = [self.store_row]
else:
self._layout = [(self.select_colour,), self.store_row]
self.embed = self.make_embed()
def make_embed(self):
"""
Embed for this shop.
"""
t = self.bot.translator.t
if self.shop.items:
owned = self.shop.owned()
lines = []
for i, item in enumerate(self.shop.items):
if owned is not None and item.itemid == owned.itemid:
line = t(_p(
'ui:colourstore|embed|line:owned_item',
"`[{j:02}]` | `{price} LC` | {mention} (You own this!)"
)).format(j=i+1, price=item.price, mention=item.mention)
else:
line = t(_p(
'ui:colourstore|embed|line:item',
"`[{j:02}]` | `{price} LC` | {mention}"
)).format(j=i+1, price=item.price, mention=item.mention)
lines.append(line)
description = '\n'.join(lines)
else:
description = t(_p(
'ui:colourstore|embed|desc',
"No colour roles available for purchase!"
))
embed = discord.Embed(
title=t(_p('ui:colourstore|embed|title', "Colour Role Shop")),
description=description
)
return embed