Files
croccybot/bot/modules/economy/shop_cmds.py

247 lines
7.7 KiB
Python

import asyncio
import discord
from collections import defaultdict
from cmdClient.checks import in_guild
from .module import module
from .shop_core import ShopItem, ShopItemType, ColourRole
from wards import is_guild_admin
class ShopSession:
__slots__ = (
'key', 'response',
'_event', '_task'
)
_sessions = {}
def __init__(self, userid, channelid):
# Build unique context key for shop session
self.key = (userid, channelid)
self.response = None
# Cancel any existing sessions
if self.key in self._sessions:
self._sessions[self.key].cancel()
self._event = asyncio.Event()
self._task = None
# Add self to the session list
self._sessions[self.key] = self
@classmethod
def get(cls, userid, channelid) -> 'ShopSession':
"""
Get a ShopSession matching the given key, if it exists.
Otherwise, returns None.
"""
return cls._sessions.get((userid, channelid), None)
async def wait(self, timeout=None):
"""
Wait for a buy response. Return the set response or raise an appropriate exception.
"""
self._task = asyncio.create_task(self._event.wait())
try:
await asyncio.wait_for(self._task, timeout=timeout)
except asyncio.CancelledError:
# Session was cancelled, likely due to creation of a new session
raise
except asyncio.TimeoutError:
# Session timed out, likely due to reaching the timeout
raise
finally:
if self._sessions.get(self.key, None) == self:
self._sessions.pop(self.key, None)
return self.response
def set(self, response):
"""
Set response.
"""
self.response = response
self._event.set()
def cancel(self):
"""
Cancel a session.
"""
if self._task:
if self._sessions.get(self.key, None) == self:
self._sessions.pop(self.key, None)
self._task.cancel()
else:
raise ValueError("Cancelling a ShopSession that is already completed!")
@module.cmd(
'shop',
group="Economy",
desc="Open the guild shop.",
flags=('add==', 'remove==', 'clear')
)
@in_guild()
async def cmd_shop(ctx, flags):
"""
Usage``:
{prefix}shop
{prefix}shop --add <price>, <item>
{prefix}shop --remove
{prefix}shop --remove itemid, itemid, ...
{prefix}shop --remove itemname, itemname, ...
{prefix}shop --clear
Description:
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
# 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
shop_items = ShopItem.fetch_where(guildid=ctx.guild.id, deleted=False, purchasable=True)
if not shop_items and not modifying:
# TODO: (FUTURE) Add tip for guild admins about setting up
return await ctx.error_reply(
"Nothing to buy! Please come back later."
)
# Categorise items
item_cats = defaultdict(list)
for item in shop_items:
item_cats[item.item_type].append(item)
# If there is more than one category, ask for which category they want
# All FUTURE TODO stuff, to be refactored into a shop widget
# item_type = None
# if ctx.args:
# # Assume category has been entered
# ...
# elif len(item_cats) > 1:
# # 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:
items = [item for item in item_cats[item_type]]
embeds = item_class.cat_shop_embeds(
ctx.guild.id,
[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
out_msg = await ctx.pager(embeds, add_cancel=True)
await ctx.cancellable(out_msg, add_reaction=False)
while True:
try:
response = await ShopSession(ctx.author.id, ctx.ch.id).wait(timeout=300)
except asyncio.CancelledError:
# User opened a new session
break
except asyncio.TimeoutError:
# Session timed out. Close the shop.
# TODO: (FUTURE) time out shop session by removing hint.
try:
embed = discord.Embed(
colour=discord.Colour.red(),
description="Shop closed! (Session timed out.)"
)
await out_msg.edit(
embed=embed
)
except discord.HTTPException:
pass
break
# Selection was made
# Sanity checks
# TODO: (FUTURE) Handle more flexible ways of selecting items
if int(response.args) >= len(items):
await response.error_reply(
"No item with this number exists! Please try again."
)
continue
item = items[int(response.args)]
if item.price > ctx.alion.coins:
await response.error_reply(
"Sorry, {} costs `{}` LionCoins, but you only have `{}`!".format(
item.display_name,
item.price,
ctx.alion.coins
)
)
continue
# Run the selection and keep the shop open in case they want to buy more.
# TODO: (FUTURE) The item may no longer exist
success = await item.buy(response)
if success and not item.allow_multi_select:
try:
await out_msg.delete()
except discord.HTTPException:
pass
break
@module.cmd(
'buy',
group="Hidden",
desc="Buy an item from the guild shop."
)
@in_guild()
async def cmd_buy(ctx):
"""
Usage``:
{prefix}buy <number>
Description:
Only usable while you have a shop open (see `{prefix}shop`).
Buys the selected item from the shop.
"""
# Check relevant session exists
session = ShopSession.get(ctx.author.id, ctx.ch.id)
if session is None:
return await ctx.error_reply(
"No shop open, nothing to buy!\n"
"Please open a shop with `{prefix}shop` first.".format(prefix=ctx.best_prefix)
)
# Check input is an integer
if not ctx.args.isdigit():
return await ctx.error_reply(
"**Usage:** `{prefix}buy <number>`, for example `{prefix}buy 1`.".format(prefix=ctx.best_prefix)
)
# Pass context back to session
session.set(ctx)
# TODO: (FUTURE) Buy command short-circuiting the shop command and acting on shop sessions
# TODO: (FUTURE) Inventory command showing the user's purchases