(Economy): Completed base shop UI.
This commit is contained in:
@@ -5,7 +5,8 @@ from collections import defaultdict
|
|||||||
from cmdClient.checks import in_guild
|
from cmdClient.checks import in_guild
|
||||||
|
|
||||||
from .module import module
|
from .module import module
|
||||||
from .shop_core import ShopItem
|
from .shop_core import ShopItem, ShopItemType, ColourRole
|
||||||
|
from wards import is_guild_admin
|
||||||
|
|
||||||
|
|
||||||
class ShopSession:
|
class ShopSession:
|
||||||
@@ -80,23 +81,36 @@ class ShopSession:
|
|||||||
'shop',
|
'shop',
|
||||||
group="Economy",
|
group="Economy",
|
||||||
desc="Open the guild shop.",
|
desc="Open the guild shop.",
|
||||||
flags=('add', 'remove', 'clear')
|
flags=('add==', 'remove==', 'clear')
|
||||||
)
|
)
|
||||||
@in_guild()
|
@in_guild()
|
||||||
async def cmd_shop(ctx, flags):
|
async def cmd_shop(ctx, flags):
|
||||||
"""
|
"""
|
||||||
Usage``:
|
Usage``:
|
||||||
{prefix}shop
|
{prefix}shop
|
||||||
{prefix}shop <category>
|
{prefix}shop --add <price>, <item>
|
||||||
|
{prefix}shop --remove
|
||||||
|
{prefix}shop --remove itemid, itemid, ...
|
||||||
|
{prefix}shop --remove itemname, itemname, ...
|
||||||
|
{prefix}shop --clear
|
||||||
Description:
|
Description:
|
||||||
Opens the guild shop.
|
Opens the guild shop. Visible items may be bought using `{prefix}buy`.
|
||||||
|
|
||||||
|
*Modifying the guild shop requires administrator permissions.*
|
||||||
"""
|
"""
|
||||||
# TODO: (FUTURE) Register session (and cancel previous sessions) so we can track for buy
|
# TODO: (FUTURE) Register session (and cancel previous sessions) so we can track for buy
|
||||||
|
|
||||||
|
# Whether we are modifying the shop
|
||||||
|
modifying = any(value is not False for value in flags.values())
|
||||||
|
if modifying and not is_guild_admin(ctx.author):
|
||||||
|
return await ctx.error_reply(
|
||||||
|
"You need to be a guild admin to modify the shop!"
|
||||||
|
)
|
||||||
|
|
||||||
# Fetch all purchasable elements, this also populates the cache
|
# Fetch all purchasable elements, this also populates the cache
|
||||||
shop_items = ShopItem.fetch_where(guildid=ctx.guild.id, deleted=False, purchasable=True)
|
shop_items = ShopItem.fetch_where(guildid=ctx.guild.id, deleted=False, purchasable=True)
|
||||||
|
|
||||||
if not shop_items:
|
if not shop_items and not modifying:
|
||||||
# TODO: (FUTURE) Add tip for guild admins about setting up
|
# TODO: (FUTURE) Add tip for guild admins about setting up
|
||||||
return await ctx.error_reply(
|
return await ctx.error_reply(
|
||||||
"Nothing to buy! Please come back later."
|
"Nothing to buy! Please come back later."
|
||||||
@@ -108,21 +122,38 @@ async def cmd_shop(ctx, flags):
|
|||||||
item_cats[item.item_type].append(item)
|
item_cats[item.item_type].append(item)
|
||||||
|
|
||||||
# If there is more than one category, ask for which category they want
|
# If there is more than one category, ask for which category they want
|
||||||
item_type = None
|
# All FUTURE TODO stuff, to be refactored into a shop widget
|
||||||
if len(item_cats) > 1:
|
# item_type = None
|
||||||
# TODO
|
# if ctx.args:
|
||||||
item_type = ...
|
# # Assume category has been entered
|
||||||
...
|
# ...
|
||||||
else:
|
# elif len(item_cats) > 1:
|
||||||
item_type = next(iter(item_cats))
|
# # TODO: Show selection menu
|
||||||
|
# item_type = ...
|
||||||
|
# ...
|
||||||
|
# else:
|
||||||
|
# # Pick the next one automatically
|
||||||
|
# item_type = next(iter(item_cats))
|
||||||
|
|
||||||
|
item_type = ShopItemType.COLOUR_ROLE
|
||||||
|
item_class = ColourRole
|
||||||
|
|
||||||
if item_type is not None:
|
if item_type is not None:
|
||||||
items = [item for item in item_cats[item_type]]
|
items = [item for item in item_cats[item_type]]
|
||||||
embeds = items[0].cat_shop_embeds(
|
embeds = item_class.cat_shop_embeds(
|
||||||
ctx.guild.id,
|
ctx.guild.id,
|
||||||
[item.itemid for item in items]
|
[item.itemid for item in items]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if modifying:
|
||||||
|
if flags['add']:
|
||||||
|
await item_class.parse_add(ctx, flags['add'])
|
||||||
|
elif flags['remove'] is not False:
|
||||||
|
await item_class.parse_remove(ctx, flags['remove'], items)
|
||||||
|
elif flags['clear']:
|
||||||
|
await item_class.parse_clear(ctx)
|
||||||
|
return
|
||||||
|
|
||||||
# Present shop pages
|
# Present shop pages
|
||||||
out_msg = await ctx.pager(embeds, add_cancel=True)
|
out_msg = await ctx.pager(embeds, add_cancel=True)
|
||||||
await ctx.cancellable(out_msg, add_reaction=False)
|
await ctx.cancellable(out_msg, add_reaction=False)
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import re
|
||||||
|
import asyncio
|
||||||
from typing import List
|
from typing import List
|
||||||
import datetime
|
import datetime
|
||||||
import discord
|
import discord
|
||||||
|
|
||||||
from cmdClient.lib import SafeCancellation
|
from cmdClient.lib import SafeCancellation
|
||||||
from meta import client
|
from meta import client
|
||||||
|
from utils.lib import multiselect_regex, parse_ranges
|
||||||
|
|
||||||
from .ShopItem import ShopItem, ShopItemType
|
from .ShopItem import ShopItem, ShopItemType
|
||||||
from .data import shop_items, shop_item_info, colour_roles
|
from .data import shop_items, shop_item_info, colour_roles
|
||||||
@@ -27,6 +30,10 @@ class ColourRole(ShopItem):
|
|||||||
def roleid(self) -> int:
|
def roleid(self) -> int:
|
||||||
return self.data.roleid
|
return self.data.roleid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def role(self) -> discord.Role:
|
||||||
|
return self.guild.get_role(self.roleid)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls, guildid, price, roleid, **kwargs):
|
async def create(cls, guildid, price, roleid, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -45,7 +52,7 @@ class ColourRole(ShopItem):
|
|||||||
|
|
||||||
# Shop interface
|
# Shop interface
|
||||||
@classmethod
|
@classmethod
|
||||||
def _cat_shop_embed_items(cls, items: List['ColourRole'], **kwargs) -> List[discord.Embed]:
|
def _cat_shop_embed_items(cls, items: List['ColourRole'], hint: str = buy_hint, **kwargs) -> List[discord.Embed]:
|
||||||
"""
|
"""
|
||||||
Embed a list of items specifically for displaying in the shop.
|
Embed a list of items specifically for displaying in the shop.
|
||||||
Subclasses will usually extend or override this, if only to add metadata.
|
Subclasses will usually extend or override this, if only to add metadata.
|
||||||
@@ -56,7 +63,7 @@ class ColourRole(ShopItem):
|
|||||||
embeds = cls._cat_embed_items(items, **kwargs)
|
embeds = cls._cat_embed_items(items, **kwargs)
|
||||||
for embed in embeds:
|
for embed in embeds:
|
||||||
embed.title = "{} shop!".format(cls.item_type.desc)
|
embed.title = "{} shop!".format(cls.item_type.desc)
|
||||||
embed.description += "\n\n" + cls.buy_hint
|
embed.description += "\n\n" + hint
|
||||||
else:
|
else:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title="{} shop!".format(cls.item_type.desc),
|
title="{} shop!".format(cls.item_type.desc),
|
||||||
@@ -171,3 +178,192 @@ class ColourRole(ShopItem):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Shop admin interface
|
||||||
|
@classmethod
|
||||||
|
async def parse_add(cls, ctx, args):
|
||||||
|
"""
|
||||||
|
Parse a request to add colour roles.
|
||||||
|
Syntax: `<price>, <role>`, with different lines treated as different entries.
|
||||||
|
|
||||||
|
Assumes the author is an admin.
|
||||||
|
"""
|
||||||
|
if not args:
|
||||||
|
raise SafeCancellation("No arguments given, nothing to do!")
|
||||||
|
|
||||||
|
lines = args.splitlines()
|
||||||
|
to_add = [] # List of (price, role) tuples to add
|
||||||
|
for line in lines:
|
||||||
|
# Syntax: <price>, <role>
|
||||||
|
splits = line.split(',')
|
||||||
|
splits = [split.strip() for split in splits]
|
||||||
|
if len(splits) < 2 or not splits[0].isdigit():
|
||||||
|
raise SafeCancellation("**Syntax:** `--add <price>, <role>`")
|
||||||
|
price = int(splits[0])
|
||||||
|
role = await ctx.find_role(splits[1], create=True, interactive=True, allow_notfound=False)
|
||||||
|
to_add.append((price, role))
|
||||||
|
|
||||||
|
# Add the roles to data
|
||||||
|
for price, role in to_add:
|
||||||
|
# TODO: Batch update would be best
|
||||||
|
await cls.create(ctx.guild.id, price, role.id)
|
||||||
|
|
||||||
|
# Report back
|
||||||
|
if len(to_add) > 1:
|
||||||
|
await ctx.reply(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Shop Updated",
|
||||||
|
description="Added `{}` new colours to the shop!".format(len(to_add))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ctx.reply(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Shop Updated",
|
||||||
|
description="Added {} to the shop for `{}` coins.".format(to_add[0][1].mention, to_add[0][0])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def parse_remove(cls, ctx, args, items):
|
||||||
|
"""
|
||||||
|
Parse a request to remove colour roles.
|
||||||
|
Syntax: `<ids>` or `<command separated roles>`
|
||||||
|
|
||||||
|
Assumes the author is an admin.
|
||||||
|
"""
|
||||||
|
if not items:
|
||||||
|
raise SafeCancellation("Colour shop is empty, nothing to delete!")
|
||||||
|
|
||||||
|
to_delete = []
|
||||||
|
if args:
|
||||||
|
if re.search(multiselect_regex, args):
|
||||||
|
# ids were selected
|
||||||
|
indexes = parse_ranges(args)
|
||||||
|
to_delete = [items[index] for index in indexes if index < len(items)]
|
||||||
|
|
||||||
|
if not to_delete:
|
||||||
|
# Assume comma separated list of roles
|
||||||
|
splits = args.split(',')
|
||||||
|
splits = [split.strip() for split in splits]
|
||||||
|
available_roles = (item.role for item in items)
|
||||||
|
available_roles = [role for role in available_roles if role]
|
||||||
|
roles = [
|
||||||
|
await ctx.find_role(rolestr, collection=available_roles, interactive=True, allow_notfound=False)
|
||||||
|
for rolestr in splits
|
||||||
|
]
|
||||||
|
roleids = set(role.id for role in roles)
|
||||||
|
to_delete = [item for item in items if item.roleid in roleids]
|
||||||
|
else:
|
||||||
|
# Interactive multi-selector
|
||||||
|
itemids = [item.itemid for item in items]
|
||||||
|
embeds = cls.cat_shop_embeds(
|
||||||
|
ctx.guild.id,
|
||||||
|
itemids,
|
||||||
|
hint=("Please select colour(s) ids to remove, or `c` to cancel.\n"
|
||||||
|
"(Respond with e.g. `1, 2, 3` or `1-3`.)")
|
||||||
|
)
|
||||||
|
out_msg = await ctx.pager(embeds)
|
||||||
|
|
||||||
|
def check(msg):
|
||||||
|
valid = msg.channel == ctx.ch and msg.author == ctx.author
|
||||||
|
valid = valid and (re.search(multiselect_regex, msg.content) or msg.content.lower() == 'c')
|
||||||
|
return valid
|
||||||
|
|
||||||
|
try:
|
||||||
|
message = await ctx.client.wait_for('message', check=check, timeout=60)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
await out_msg.delete()
|
||||||
|
await ctx.error_reply("Session timed out. No colour roles were removed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
await out_msg.delete()
|
||||||
|
await message.delete()
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if message.content.lower() == 'c':
|
||||||
|
return
|
||||||
|
|
||||||
|
to_delete = [
|
||||||
|
items[index]
|
||||||
|
for index in parse_ranges(message.content) if index < len(items)
|
||||||
|
]
|
||||||
|
if not to_delete:
|
||||||
|
raise SafeCancellation("Nothing to delete!")
|
||||||
|
|
||||||
|
# Build an ack string before we delete
|
||||||
|
rolestr = to_delete[0].role.mention if to_delete[0].role else "`{}`".format(to_delete[0].roleid)
|
||||||
|
|
||||||
|
# Delete the items
|
||||||
|
shop_items.delete_where(itemid=[item.itemid for item in to_delete])
|
||||||
|
|
||||||
|
# Update the info cache
|
||||||
|
[shop_item_info.row_cache.pop(item.itemid, None) for item in to_delete]
|
||||||
|
|
||||||
|
# Ack and log
|
||||||
|
if len(to_delete) > 1:
|
||||||
|
try:
|
||||||
|
await ctx.reply(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Colour Roles removed",
|
||||||
|
description="You have removed `{}` colour roles.".format(len(to_delete)),
|
||||||
|
colour=discord.Colour.orange()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
event_log = ctx.guild_settings.event_log.value
|
||||||
|
if event_log:
|
||||||
|
try:
|
||||||
|
await event_log.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Colour Roles deleted",
|
||||||
|
description="{} removed `{}` colour roles from the shop.".format(
|
||||||
|
ctx.author.mention,
|
||||||
|
len(to_delete)
|
||||||
|
),
|
||||||
|
timestamp=datetime.datetime.utcnow()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
await ctx.reply(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Colour Role removed",
|
||||||
|
description="You have removed the colour role {}.".format(rolestr),
|
||||||
|
colour=discord.Colour.orange()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
event_log = ctx.guild_settings.event_log.value
|
||||||
|
if event_log:
|
||||||
|
try:
|
||||||
|
await event_log.send(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title="Colour Role deleted",
|
||||||
|
description="{} removed the colour role {} from the shop.".format(
|
||||||
|
ctx.author.mention,
|
||||||
|
rolestr
|
||||||
|
),
|
||||||
|
timestamp=datetime.datetime.utcnow()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def parse_clear(cls, ctx):
|
||||||
|
"""
|
||||||
|
Parse a request to clear colour roles.
|
||||||
|
|
||||||
|
Assumes the author is an admin.
|
||||||
|
"""
|
||||||
|
if await ctx.ask("Are you sure you want to remove all colour roles from the shop?"):
|
||||||
|
shop_items.delete_where(guildid=ctx.guild.id, item_type=cls.item_type)
|
||||||
|
await ctx.reply("All colour roles deleted.")
|
||||||
|
await ctx.guild_settings.event_log.log("{} cleared the colour role shop.".format(ctx.author.mention))
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class ShopItem:
|
|||||||
item_type = None # type: ShopItemType
|
item_type = None # type: ShopItemType
|
||||||
|
|
||||||
# Format string to use for each item of this type in the shop embed
|
# Format string to use for each item of this type in the shop embed
|
||||||
shop_fmt = "`[{num:<{num_len}}]` | `{item.price:<{price_len}}` {item.display_name}"
|
shop_fmt = "`[{num:<{num_len}}]` | `{item.price:<{price_len}} LC` {item.display_name}"
|
||||||
|
|
||||||
# Shop input modifiers
|
# Shop input modifiers
|
||||||
allow_multi_select = True
|
allow_multi_select = True
|
||||||
@@ -248,7 +248,7 @@ class ShopItem:
|
|||||||
# Ensure cache is populated
|
# Ensure cache is populated
|
||||||
shop_item_info.fetch_rows_where(itemid=itemids)
|
shop_item_info.fetch_rows_where(itemid=itemids)
|
||||||
|
|
||||||
return cls._cat_shop_embed_items([cls(itemid) for itemid in itemids])
|
return cls._cat_shop_embed_items([cls(itemid) for itemid in itemids], **kwargs)
|
||||||
|
|
||||||
async def buy(self, ctx):
|
async def buy(self, ctx):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from . import data
|
from . import data
|
||||||
|
|
||||||
from .ShopItem import ShopItem
|
from .ShopItem import ShopItem, ShopItemType
|
||||||
from .ColourRole import ColourRole
|
from .ColourRole import ColourRole
|
||||||
|
|||||||
@@ -510,3 +510,9 @@ class FieldEnum(str, Enum):
|
|||||||
|
|
||||||
def __conform__(self, proto):
|
def __conform__(self, proto):
|
||||||
return QuotedString(self.value)
|
return QuotedString(self.value)
|
||||||
|
|
||||||
|
|
||||||
|
multiselect_regex = re.compile(
|
||||||
|
r"^([0-9, -]+)$",
|
||||||
|
re.DOTALL | re.IGNORECASE | re.VERBOSE
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user