Ruby-Cogs/shop/shop.py

2016 lines
85 KiB
Python

# Shop was made by Redjumpman for Red Bot.
# Standard Library
import asyncio
import csv
import logging
import random
import textwrap
import uuid
from bisect import bisect
from copy import deepcopy
from itertools import zip_longest
from typing import Literal
# Shop
from .ui import ShopView, InventoryView, PurchaseView, UseItemView
from .inventory import Inventory
from .checks import Checks
# Discord.py
import discord
# Red
from redbot.core import Config, bank, commands
from redbot.core.utils import AsyncIter
from redbot.core.utils.chat_formatting import humanize_list
from redbot.core.data_manager import bundled_data_path
from redbot.core.errors import BalanceTooHigh
log = logging.getLogger("red.shop")
__version__ = "3.2.0"
__author__ = "Redjumpman"
def global_permissions():
async def pred(ctx: commands.Context):
is_owner = await ctx.bot.is_owner(ctx.author)
if is_owner:
return True
if not await Shop().shop_is_global() and ctx.guild:
permissions = ctx.channel.permissions_for(ctx.author)
admin_roles = await ctx.bot._config.guild(ctx.guild).admin_role()
author_roles = [role.id for role in ctx.author.roles]
admin_role_check = check_if_role_in_roles(admin_roles, author_roles)
return admin_role_check or (ctx.author == ctx.guild.owner) or permissions.administrator
return commands.check(pred)
def check_if_role_in_roles(admin_roles, user_roles):
intersection = list(set(admin_roles).intersection(user_roles))
if not intersection:
return False
return True
class Shop(commands.Cog):
shop_defaults = {
"Shops": {},
"Settings": {"Alerts": False, "Alert_Role": "Admin", "Closed": False, "Gifting": True, "Sorting": "price",},
"Pending": {},
}
member_defaults = {
"Inventory": {},
"Trading": True,
}
user_defaults = member_defaults
global_defaults = deepcopy(shop_defaults)
global_defaults["Global"] = False
def __init__(self):
self.config = Config.get_conf(self, 5074395003, force_registration=True)
self.config.register_guild(**self.shop_defaults)
self.config.register_global(**self.global_defaults)
self.config.register_member(**self.member_defaults)
self.config.register_user(**self.user_defaults)
self.ready = False
async def initialize(self):
"""Initialize the shop cog by loading default shops if none exist."""
if self.ready:
return
# Check if any shops exist
if not await self.config.Shops():
# Load default shop from CSV
try:
fp = bundled_data_path(self) / "basic_shop.csv"
if fp.exists():
parser = Parser(None, self.config, None)
await parser.search_csv(fp)
log.info("Loaded default shop configuration")
except Exception as e:
log.error(f"Failed to load default shop: {e}")
log.exception("Full traceback:")
self.ready = True
async def red_delete_data_for_user(
self, *, requester: Literal["discord", "owner", "user", "user_strict"], user_id: int
):
await self.config.user_from_id(user_id).clear()
all_members = await self.config.all_members()
async for guild_id, guild_data in AsyncIter(all_members.items(), steps=100):
if user_id in guild_data:
await self.config.member_from_ids(guild_id, user_id).clear()
# -----------------------COMMANDS-------------------------------------
@commands.command()
async def inventory(self, ctx):
"""Displays your purchased items."""
try:
instance = await self.get_instance(ctx, user=ctx.author)
except AttributeError:
return await ctx.message.reply("You can't use this command in DMs when not in global mode.")
inventory = await instance.Inventory.all()
if not inventory:
embed = discord.Embed(
title="Empty Inventory",
description="Your inventory is empty.",
color=discord.Color.red()
)
return await ctx.message.reply(embed=embed)
embed = discord.Embed(
title=f"{ctx.author.display_name}'s Inventory",
color=discord.Color.blue()
)
view = InventoryView(ctx, inventory)
view.message = await ctx.message.reply(embed=embed, view=view)
try:
await view.wait()
if view.selected_item: # An item was selected to use
await self.pending_prompt(ctx, instance, inventory, view.selected_item)
except asyncio.TimeoutError:
pass # Handled by view's on_timeout
async def inv_hook(self, user):
"""Inventory Hook for outside cogs
Parameters
----------
user : discord.Member or discord.User
Returns
-------
dict
Returns a dict of the user's inventory or empty if the user
is not found.
"""
try:
instance = await self._inv_hook_instance(user)
except AttributeError:
return {}
else:
return await instance.Inventory.all()
async def _inv_hook_instance(self, user):
if await self.config.Global():
return self.config.user(user)
else:
return self.config.member(user)
@commands.group(autohelp=True)
async def shop(self, ctx):
"""Shop group command"""
pass
@shop.command()
async def buy(self, ctx, *purchase):
"""Opens the shop menu or directly purchases an item."""
try:
instance = await self.get_instance(ctx, settings=True)
except AttributeError:
return await ctx.message.reply("You can't use this command in DMs when not in global mode.")
if not await instance.Shops():
embed = discord.Embed(
title="No Shops Available",
description="No shops have been created yet.",
color=discord.Color.red()
)
return await ctx.message.reply(embed=embed)
if await instance.Settings.Closed():
embed = discord.Embed(
title="Shops Closed",
description="The shop system is currently closed.",
color=discord.Color.red()
)
return await ctx.message.reply(embed=embed)
shops = await instance.Shops.all()
available_shops = await self.check_availability(ctx, shops)
if not available_shops:
embed = discord.Embed(
title="No Access",
description="Either no items have been created, you need a higher role, or this command should be used in a server and not DMs.",
color=discord.Color.red()
)
return await ctx.message.reply(embed=embed)
if purchase:
try:
shop, item = purchase
except ValueError:
embed = discord.Embed(
title="Invalid Parameters",
description="Too many parameters passed. Use help on this command for more information.",
color=discord.Color.red()
)
return await ctx.message.reply(embed=embed)
if shop not in shops:
embed = discord.Embed(
title="Shop Not Found",
description="Either that shop does not exist, or you don't have access to it.",
color=discord.Color.red()
)
return await ctx.message.reply(embed=embed)
item_data = shops[shop]["Items"].get(item)
if not item_data:
embed = discord.Embed(
title="Item Not Found",
description=f"Item '{item}' not found in shop '{shop}'.",
color=discord.Color.red()
)
return await ctx.message.reply(embed=embed)
view = PurchaseView(ctx, shop, item, item_data)
embed = view.build_embed()
view.message = await ctx.message.reply(embed=embed, view=view)
try:
await view.wait()
if view.quantity: # A purchase was initiated
user_data = await self.get_instance(ctx, user=ctx.author)
sm = ShopManager(ctx, instance, user_data)
await sm.order(shop, item, view.quantity)
except asyncio.TimeoutError:
pass # Handled by view's on_timeout
else:
# Open interactive shop menu
embed = discord.Embed(
title="Welcome to the Shop",
description=f"Welcome {ctx.author.mention}! Please select a shop to browse.",
color=discord.Color.blue()
)
view = ShopView(ctx, shops)
view.message = await ctx.message.reply(embed=embed, view=view)
try:
await view.wait()
if view.current_shop and view.current_item and view.quantity:
user_data = await self.get_instance(ctx, user=ctx.author)
sm = ShopManager(ctx, instance, user_data)
await sm.order(view.current_shop, view.current_item, view.quantity)
except asyncio.TimeoutError:
pass # Handled by view's on_timeout
@shop.command()
@commands.max_concurrency(1, commands.BucketType.user)
async def redeem(self, ctx, *, item: str):
"""Redeems an item in your inventory."""
try:
instance = await self.get_instance(ctx, user=ctx.author)
except AttributeError:
return await ctx.message.reply("You can't use this command in DMs when not in global mode.")
data = await instance.Inventory.all()
if data is None:
return await ctx.message.reply("Your inventory is empty.")
if item not in data:
return await ctx.message.reply("You don't own this item.")
await self.pending_prompt(ctx, instance, data, item)
@shop.command()
@commands.guild_only()
@commands.cooldown(1, 5, commands.BucketType.user)
async def trade(self, ctx, user: discord.Member, quantity: int, *, item: str):
"""Attempts to trade an item with another user."""
cancel = ctx.prefix + "cancel"
author_instance = await self.get_instance(ctx, user=ctx.author)
author_inventory = await author_instance.Inventory.all()
user_instance = await self.get_instance(ctx, user=user)
user_inv = await user_instance.Inventory.all()
if not await user_instance.Trading():
return await ctx.message.reply("This user has trading turned off.")
if item not in author_inventory:
return await ctx.message.reply("You don't own that item.")
if 0 < author_inventory[item]["Qty"] < quantity:
return await ctx.message.reply("You don't have that many {}".format(item))
await ctx.message.reply(
"{} has requested a trade with {}.\n"
"They are offering {}x {}.\n Do wish to trade?\n"
"*This trade can be canceled at anytime by typing `{}`.*"
"".format(ctx.author.mention, user.mention, quantity, item, cancel)
)
def check(m):
return (m.author == user and m.content.lower() in ("yes", "no", cancel)) or (
m.author == ctx.author and m.content.lower() == cancel
)
try:
decision = await ctx.bot.wait_for("message", timeout=25, check=check)
except asyncio.TimeoutError:
return await ctx.message.reply("Trade request timed out. Canceled trade.")
if decision.content.lower() in ("no", cancel):
return await ctx.message.reply("Trade canceled.")
await ctx.message.reply("{} What is your counter offer?\n" '*Example: 3 "Healing Potions"*'.format(user.mention))
def predicate(m):
if m.author in (user, ctx.author) and m.content == cancel:
return True
if m.author != user:
return False
try:
q, i = [x.strip() for x in m.content.split('"')[:2] if x]
except ValueError:
return False
else:
if i not in user_inv:
return False
return 0 < user_inv[i]["Qty"] <= int(q)
try:
offer = await ctx.bot.wait_for("message", timeout=25, check=predicate)
except asyncio.TimeoutError:
return await ctx.message.reply("Trade request timed out. Canceled trade.")
if offer.content.lower() == cancel:
return await ctx.message.reply("Trade canceled.")
qty, item2 = [x.strip() for x in offer.content.split('"')[:2] if x]
await ctx.message.reply(
"{} Do you wish to trade {}x {} for {}'s {}x {}?"
"".format(ctx.author.mention, quantity, item, user.mention, qty, item2)
)
def check2(m):
return (m.author == ctx.author and m.content.lower() in ("yes", "no", cancel)) or (
m.author == user and m.content.lower() == cancel
)
try:
final = await ctx.bot.wait_for("message", timeout=25, check=check2)
except asyncio.TimeoutError:
return await ctx.message.reply("Trade request timed out. Canceled trade.")
if final.content.lower() in ("no", cancel):
return await ctx.message.reply("Trade canceled.")
sm1 = ShopManager(ctx, instance=None, user_data=author_instance)
await sm1.remove(item, number=quantity)
sm2 = ShopManager(ctx, instance=None, user_data=user_instance)
await sm2.add(item, author_inventory[item], quantity)
await sm2.remove(item2, number=int(qty))
await ctx.message.reply("Trade complete.")
@shop.command()
async def tradetoggle(self, ctx):
"""Disables or enables trading with you."""
try:
instance = await self.get_instance(ctx, user=ctx.author)
except AttributeError:
return await ctx.send("You can't use this command in DMs when not in global mode.")
status = await instance.Trading()
await instance.Trading.set(not status)
await ctx.send("Trading with you is now {}.".format("disabled" if status else "enabled"))
@shop.command()
async def refund(self, ctx, *, item: str):
"""Refunds a role item back to your inventory."""
try:
settings = await self.get_instance(ctx, settings=True)
user_instance = await self.get_instance(ctx, user=ctx.author)
except AttributeError:
return await ctx.message.reply("You can't use this command in DMs when not in global mode.")
# Find the role item in any shop
role_item = None
role_name = None
async with settings.Shops() as shops:
for shop in shops.values():
for item_name, item_data in shop["Items"].items():
if (item_name.lower() == item.lower() and
item_data["Type"].lower() == "role"):
role_item = item_name
role_name = item_data["Role"]
item_data = deepcopy(item_data)
break
if role_item:
break
if not role_item:
return await ctx.message.reply(
embed=discord.Embed(
title="Item Not Found",
description=f"Could not find a role item named '{item}' in any shop.",
color=discord.Color.red()
)
)
# Check if user has the role
if role_name.startswith('<@&') and role_name.endswith('>'):
role_id = int(role_name.strip('<@&>'))
role = ctx.guild.get_role(role_id)
else:
role = discord.utils.get(ctx.guild.roles, name=role_name)
if not role:
return await ctx.message.reply(
embed=discord.Embed(
title="Role Not Found",
description=f"The role associated with this item no longer exists on the server.",
color=discord.Color.red()
)
)
if role not in ctx.author.roles:
return await ctx.message.reply(
embed=discord.Embed(
title="Role Not Owned",
description=f"You don't have the `{role.name}` role to refund.",
color=discord.Color.red()
)
)
# Ask for confirmation
embed = discord.Embed(
title="Confirm Role Refund",
description=f"Are you sure you want to remove the `{role.name}` role and get `{role_item}` back in your inventory?",
color=discord.Color.blue()
)
msg = await ctx.message.reply(embed=embed)
# Add reactions for yes/no
await msg.add_reaction("")
await msg.add_reaction("")
def check(reaction, user):
return user == ctx.author and str(reaction.emoji) in ["", ""]
try:
reaction, user = await ctx.bot.wait_for("reaction_add", timeout=30.0, check=check)
if str(reaction.emoji) == "":
try:
await ctx.author.remove_roles(role, reason="Shop role refund")
# Add the role item back to inventory
async with user_instance.Inventory() as inv:
if role_item in inv:
inv[role_item]["Qty"] += 1
else:
inv[role_item] = {
"Qty": 1,
"Type": "role",
"Info": item_data["Info"],
"Role": role_name
}
embed = discord.Embed(
title="Role Refunded",
description=f"The `{role.name}` role has been removed and `{role_item}` has been added to your inventory.",
color=discord.Color.green()
)
await msg.edit(embed=embed)
except discord.Forbidden:
embed = discord.Embed(
title="Refund Failed",
description="I don't have permission to remove that role.",
color=discord.Color.red()
)
await msg.edit(embed=embed)
else:
embed = discord.Embed(
title="Refund Cancelled",
description="Role refund cancelled.",
color=discord.Color.red()
)
await msg.edit(embed=embed)
except asyncio.TimeoutError:
embed = discord.Embed(
title="Timed Out",
description="Role refund timed out.",
color=discord.Color.red()
)
await msg.edit(embed=embed)
@shop.command()
async def version(self, ctx):
"""Shows the current Shop version."""
await ctx.message.reply("Shop is running version {}.".format(__version__))
@shop.command()
@commands.is_owner()
async def wipe(self, ctx):
"""Wipes all shop cog data."""
await ctx.message.reply(
"You are about to delete all shop and user data from the bot. Are you sure this is what you wish to do?"
)
try:
choice = await ctx.bot.wait_for("message", timeout=25.0, check=Checks(ctx).confirm)
except asyncio.TimeoutError:
return await ctx.message.reply("No Response. Action canceled.")
if choice.content.lower() == "yes":
await self.config.clear_all()
msg = "{0.name} ({0.id}) wiped all shop data.".format(ctx.author)
log.info(msg)
await ctx.message.reply(msg)
else:
return await ctx.message.reply("Wipe canceled.")
@shop.command()
@global_permissions()
@commands.guild_only()
@commands.max_concurrency(1, commands.BucketType.user)
async def pending(self, ctx):
"""Displays the pending menu."""
instance = await self.get_instance(ctx, settings=True)
if not await instance.Pending():
return await ctx.send("There are not any pending items.")
data = await instance.Pending.all()
class PendingView(discord.ui.View):
def __init__(self, cog, ctx, data, timeout=60):
super().__init__(timeout=timeout)
self.cog = cog
self.ctx = ctx
self.data = data
self.setup_user_select()
def setup_user_select(self):
options = []
for user_id, items in self.data.items():
user = self.ctx.bot.get_user(int(user_id))
if user:
desc = f"{len(items)} pending items"
options.append(discord.SelectOption(
label=user.name,
description=desc,
value=user_id
))
if options:
user_select = discord.ui.Select(
placeholder="Select a user",
options=options[:25], # Discord limit
row=0
)
user_select.callback = self.user_selected
self.add_item(user_select)
async def user_selected(self, interaction: discord.Interaction):
if interaction.user != self.ctx.author:
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
user_id = interaction.data["values"][0]
user_items = self.data[user_id]
# Clear previous item select if it exists
for item in self.children[:]:
if isinstance(item, discord.ui.Select) and item.row == 1:
self.remove_item(item)
# Add item select for this user
options = []
for item_id, item_data in user_items.items():
options.append(discord.SelectOption(
label=item_data["Item"][:25],
description=f"ID: {item_id[:8]}...",
value=f"{user_id}:{item_id}"
))
if options:
item_select = discord.ui.Select(
placeholder="Select an item to process",
options=options[:25],
row=1
)
item_select.callback = self.item_selected
self.add_item(item_select)
await interaction.response.edit_message(view=self)
async def item_selected(self, interaction: discord.Interaction):
if interaction.user != self.ctx.author:
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
user_id, item_id = interaction.data["values"][0].split(":")
user = self.ctx.bot.get_user(int(user_id))
item_data = self.data[user_id][item_id]
# Create confirmation view
confirm_view = PendingConfirmView(self.cog, user, item_id, item_data["Item"])
embed = discord.Embed(
title="Pending Item",
description=f"Process pending item for {user.mention}",
color=discord.Color.blue()
)
embed.add_field(name="Item", value=item_data["Item"])
embed.add_field(name="ID", value=item_id)
embed.add_field(name="Timestamp", value=item_data["Timestamp"])
await interaction.response.edit_message(embed=embed, view=confirm_view)
class PendingConfirmView(discord.ui.View):
def __init__(self, cog, user, item_id, item_name, timeout=60):
super().__init__(timeout=timeout)
self.cog = cog
self.user = user
self.item_id = item_id
self.item_name = item_name
@discord.ui.button(label="Approve", style=discord.ButtonStyle.green)
async def approve(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != ctx.author:
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
async with instance.Pending() as p:
del p[str(self.user.id)][self.item_id]
if not p[str(self.user.id)]:
del p[str(self.user.id)]
await interaction.response.edit_message(
content=f"{self.item_name} was approved for {self.user.name}.",
embed=None,
view=None
)
try:
await self.user.send(f"{ctx.author.name} approved your pending {self.item_name}!")
except discord.HTTPException:
pass
self.stop()
@discord.ui.button(label="Deny", style=discord.ButtonStyle.red)
async def deny(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != ctx.author:
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
async with instance.Pending() as p:
del p[str(self.user.id)][self.item_id]
if not p[str(self.user.id)]:
del p[str(self.user.id)]
await interaction.response.edit_message(
content=f"{self.item_name} was denied for {self.user.name}.",
embed=None,
view=None
)
try:
await self.user.send(f"{ctx.author.name} denied your pending {self.item_name}.")
except discord.HTTPException:
pass
self.stop()
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.grey)
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != ctx.author:
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
await interaction.response.edit_message(content="Action cancelled.", embed=None, view=None)
self.stop()
# Start the pending menu with ctx passed
view = PendingView(self, ctx, data)
await ctx.send(
"Select a user to view their pending items:",
view=view
)
@shop.command()
@global_permissions()
@commands.guild_only()
async def gift(self, ctx, user: discord.Member, quantity: int, *, item):
"""Gift another user a set number of one of your items."""
if quantity < 1:
return await ctx.message.reply(":facepalm: How would that work genius?")
if user == ctx.author:
return await ctx.message.reply("Really? Maybe you should find some friends.")
settings = await self.get_instance(ctx, settings=True)
if not await settings.Settings.Gifting():
return await ctx.message.reply("Gifting is turned off.")
author_instance = await self.get_instance(ctx, user=ctx.author)
author_inv = await author_instance.Inventory.all()
if item not in author_inv:
return await ctx.message.reply(f"You don't own any `{item}`.")
if author_inv[item]["Qty"] < quantity:
return await ctx.message.reply(f"You don't have that many `{item}` to give.")
# Check if it's a role item
if author_inv[item]["Type"].lower() == "role":
if quantity > 1:
return await ctx.message.reply(
embed=discord.Embed(
title="Invalid Quantity",
description="You can only gift one copy of a role item.",
color=discord.Color.red()
)
)
# Check if recipient already has the role item
user_instance = await self.get_instance(ctx, user=user)
user_inv = await user_instance.Inventory.all()
if item in user_inv:
return await ctx.message.reply(
embed=discord.Embed(
title="Already Owned",
description=f"{user.display_name} already owns this role item. Users can only have one copy at a time.",
color=discord.Color.red()
)
)
sm1 = ShopManager(ctx, instance=None, user_data=author_instance)
await sm1.remove(item, number=quantity)
user_instance = await self.get_instance(ctx, user=user)
sm2 = ShopManager(ctx, instance=None, user_data=user_instance)
success = await sm2.add(item, author_inv[item], quantity)
if success:
await ctx.message.reply(
embed=discord.Embed(
title="Gift Successful",
description=f"{ctx.author.mention} gifted {user.mention} {quantity}x {item}.",
color=discord.Color.green()
)
)
else:
# If gift failed, give item back to original owner
await sm1.add(item, author_inv[item], quantity)
await ctx.message.reply(
embed=discord.Embed(
title="Gift Failed",
description=f"Could not gift {item} to {user.mention}. The item has been returned to your inventory.",
color=discord.Color.red()
)
)
@shop.command()
@global_permissions()
@commands.guild_only()
async def give(self, ctx, user: discord.Member, quantity: int, *shopitem):
"""Administratively gives a user an item."""
if quantity < 1:
return await ctx.message.reply(":facepalm: You can't do that.")
if shopitem is None:
return await ctx.send_help()
try:
shop, item = shopitem
except ValueError:
return await ctx.message.reply('Must be a `"Shop Name" "Item Name"` format.')
instance = await self.get_instance(ctx, settings=True)
shops = await instance.Shops.all()
if shop not in shops:
return await ctx.message.reply("Invalid shop name.")
elif item not in shops[shop]["Items"]:
return await ctx.message.reply("That item in not in the {} shop.".format(shop))
elif shops[shop]["Items"][item]["Type"] not in ("basic", "role"):
return await ctx.message.reply("You can only give basic or role type items.")
else:
data = deepcopy(shops[shop]["Items"][item])
user_instance = await self.get_instance(ctx, user=user)
sm = ShopManager(ctx, None, user_instance)
await sm.add(item, data, quantity)
await ctx.message.reply("{} just gave {} a {}.".format(ctx.author.mention, user.mention, item))
@shop.command()
@global_permissions()
@commands.guild_only()
async def clearinv(self, ctx, user: discord.Member):
"""Completely clears a user's inventory."""
await ctx.message.reply("Are you sure you want to completely wipe {}'s inventory?".format(user.name))
choice = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx).confirm)
if choice.content.lower() != "yes":
return await ctx.message.reply("Canceled inventory wipe.")
instance = await self.get_instance(ctx=ctx, user=user)
await instance.Inventory.clear()
await ctx.message.reply("Done. Inventory wiped for {}.".format(user.name))
@shop.command()
@global_permissions()
@commands.guild_only()
async def toggle(self, ctx):
"""Closes/opens all shops."""
instance = await self.get_instance(ctx, settings=True)
status = await instance.Settings.Closed()
await instance.Settings.Closed.set(not status)
await ctx.message.reply("Shops are now {}.".format("open" if status else "closed"))
@shop.command()
@global_permissions()
@commands.guild_only()
async def manager(self, ctx, action: str):
"""Creates edits, or deletes a shop."""
if action.lower() not in ("create", "edit", "delete"):
return await ctx.send("Action must be create, edit, or delete.")
instance = await self.get_instance(ctx, settings=True)
try:
if action.lower() == "create":
await self.create_shop(ctx, instance)
elif action.lower() == "edit":
await self.edit_shop(ctx, instance)
else:
await self.delete_shop(ctx, instance)
except asyncio.TimeoutError:
await ctx.send("Shop manager timed out.")
@shop.command()
@global_permissions()
@commands.guild_only()
async def item(self, ctx, action: str):
"""Creates, Deletes, and Edits items."""
if action.lower() not in ("create", "edit", "delete"):
return await ctx.send("Must pick create, edit, or delete.")
instance = await self.get_instance(ctx, settings=True)
im = ItemManager(ctx, instance)
try:
await im.run(action)
except asyncio.TimeoutError:
return await ctx.send("Request timed out. Process canceled.")
@shop.command()
@global_permissions()
@commands.guild_only()
async def restock(self, ctx, amount: int, *, shop_name: str):
"""Restocks all items in a shop by a specified amount.
This command will not restock auto items, because they are required
to have an equal number of messages and stock.
"""
instance = await self.get_instance(ctx, settings=True)
shop = shop_name
if shop not in await instance.Shops():
return await ctx.send("That shop does not exist.")
await ctx.send(
"Are you sure you wish to increase the quantity of all "
"items in {} by {}?\n*Note, this won't affect auto items.*"
"".format(shop, amount)
)
try:
choice = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx).confirm)
except asyncio.TimeoutError:
return await ctx.send("Response timed out.")
if choice.content.lower() == "yes":
async with instance.Shops() as shops:
for item in shops[shop]["Items"].values():
if item["Type"] != "auto":
try:
item["Qty"] += amount
except TypeError:
continue
await ctx.send("All items in {} have had their quantities increased by {}.".format(shop, amount))
else:
await ctx.send("Restock canceled.")
@shop.command()
@global_permissions()
@commands.guild_only()
async def bulkadd(self, ctx, style: str, *, entry: str):
"""Add multiple items and shops.
Bulk accepts two styles: text or a file. If you choose
file, then the next argument is your file name.
Files should be saved in your CogManager/cogs/shop/data path.
If you choose text, then each line will be parsed.
Each entry begins as a new line. All parameters MUST
be separated by a comma.
Parameters:
----------
Shop Name, Item Name, Type, Quantity, Cost, Info, Role
Role is only required if the type is set to role.
Examples
-------
Holy Temple, Torch, basic, 20, 5, Provides some light.
Holy Temple, Divine Training, role, 20, 500, Gives Priest role., Priest
Junkyard, Mystery Box, random, 20, 500, Random piece of junk.
For more information on the parameters visit the shop wiki.
"""
if style.lower() not in ("file", "text"):
return await ctx.send("Invalid style type. Must be file or text.")
msg = await ctx.send("Beginning bulk upload process for shop. This may take a while...")
instance = await self.get_instance(ctx, settings=True)
parser = Parser(ctx, instance, msg)
if style.lower() == "file":
if not ctx.bot.is_owner(ctx.author):
return await ctx.send("Only the owner can add items via csv files.")
fp = bundled_data_path(self) / f"{entry}.csv"
await parser.search_csv(fp)
else:
await parser.parse_text_entry(entry)
# -----------------------------------------------------------------------------
@commands.group(autohelp=True)
async def setshop(self, ctx):
"""Shop Settings group command"""
pass
@setshop.command()
@commands.is_owner()
async def mode(self, ctx):
"""Toggles Shop between global and local modes.
When shop is set to local mode, each server will have its own
unique data, and admin level commands can be used on that server.
When shop is set to global mode, data is linked between all servers
the bot is connected to. In addition, admin level commands can only be
used by the owner or co-owners.
"""
author = ctx.author
mode = "global" if await self.shop_is_global() else "local"
alt = "local" if mode == "global" else "global"
await ctx.send(
"Shop is currently set to {} mode. Would you like to change to {} mode instead?".format(mode, alt)
)
checks = Checks(ctx)
try:
choice = await ctx.bot.wait_for("message", timeout=25.0, check=checks.confirm)
except asyncio.TimeoutError:
return await ctx.send("No response. Action canceled.")
if choice.content.lower() != "yes":
return await ctx.send("Shop will remain {}.".format(mode))
await ctx.send(
"Changing shop to {0} will **DELETE ALL** current shop data. Are "
"you sure you wish to make shop {0}?".format(alt)
)
try:
final = await ctx.bot.wait_for("message", timeout=25.0, check=checks.confirm)
except asyncio.TimeoutError:
return await ctx.send("No response. Action canceled.")
if final.content.lower() == "yes":
await self.change_mode(alt)
log.info("{} ({}) changed the shop mode to {}.".format(author.name, author.id, alt))
await ctx.send("Shop data deleted! Shop mode is now set to {}.".format(alt))
else:
await ctx.send("Shop will remain {}.".format(mode))
@setshop.command()
@global_permissions()
@commands.guild_only()
async def alertrole(self, ctx, role: discord.Role):
"""Sets the role that will receive alerts.
Alerts will be sent to any user who has this role on the server. If
the shop is global, then the owner will receive alerts regardless
of their role, until they turn off alerts.
"""
if role.name == "Bot":
return
instance = await self.get_instance(ctx, settings=True)
await instance.Settings.Alert_Role.set(role.name)
await ctx.send("Alert role has been set to {}.".format(role.name))
@setshop.command()
@global_permissions()
@commands.guild_only()
async def alerts(self, ctx):
"""Toggles alerts when users redeem items."""
instance = await self.get_instance(ctx, settings=True)
status = await instance.Settings.Alerts()
await instance.Settings.Alerts.set(not status)
await ctx.send("Alert role will {} messages.".format("no longer" if status else "receive"))
@setshop.command()
@global_permissions()
@commands.guild_only()
async def sorting(self, ctx, style: str):
"""Set how shop items are sorted.
Options: price, quantity, or name (alphabetical)
By default shops are ordered by price."""
instance = await self.get_instance(ctx, settings=True)
if style not in ("price", "quantity", "name"):
return await ctx.send("You must pick a valid sorting option: `price`, `quantity`, or `name`.")
await instance.Settings.Sorting.set(style.lower())
await ctx.send(f"Shops will now be sorted by {style}.")
@setshop.command()
@global_permissions()
@commands.guild_only()
async def gifting(self, ctx):
"""Toggles if users can gift items."""
instance = await self.get_instance(ctx, settings=True)
status = await instance.Settings.Gifting()
await instance.Settings.Gifting.set(not status)
await ctx.send(f"Gifting is now {'OFF'} if status else {'ON'}.")
@setshop.command()
@global_permissions()
@commands.guild_only()
async def toggle(self, ctx):
"""Closes/opens all shops."""
instance = await self.get_instance(ctx, settings=True)
status = await instance.Settings.Closed()
await instance.Settings.Closed.set(not status)
await ctx.send("Shops are now {}.".format("open" if status else "closed"))
# -------------------------------------------------------------------------------
@staticmethod
async def check_availability(ctx, shops):
if ctx.guild:
perms = ctx.author.guild_permissions.administrator
author_roles = [r.name for r in ctx.author.roles]
return [x for x, y in shops.items() if (y["Role"] in author_roles or perms) and y["Items"]]
@staticmethod
async def clear_single_pending(ctx, instance, data, item, user):
item_name = data[str(user.id)][item]["Item"]
await ctx.send(
"You are about to clear a pending {} for {}.\nAre you sure "
"you wish to clear this item?".format(item_name, user.name)
)
choice = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx).confirm)
if choice.content.lower() == "yes":
async with instance.Pending() as p:
del p[str(user.id)][item]
if not p[str(user.id)]:
del p[str(user.id)]
await ctx.send("{} was cleared from {}'s pending by {}.".format(item_name, user.name, ctx.author.name))
await user.send("{} cleared your pending {}!".format(ctx.author.name, item_name))
else:
await ctx.send("Action canceled.")
@staticmethod
async def clear_all_pending(ctx, instance, user):
await ctx.send("You are about to clear all pending items from {}.\nAre you sure you wish to do this?")
choice = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx).confirm)
if choice.content.lower() == "yes":
async with instance.Pending() as p:
del p[user.id]
await ctx.send("All pending items have been cleared for {}.".format(user.name))
await user.send("{} cleared **ALL** of your pending items.".format(ctx.author.name))
else:
await ctx.send("Action canceled.")
async def get_instance(self, ctx, settings=False, user=None):
if not user:
user = ctx.author
if await self.config.Global():
if settings:
return self.config
else:
return self.config.user(user)
else:
if settings:
return self.config.guild(ctx.guild)
else:
return self.config.member(user)
async def assign_role(self, ctx, instance, item, role_name):
"""Assign a role to a user."""
if await self.config.Global():
if not ctx.guild:
return await ctx.message.reply(
"Unable to assign role, because shop is in global mode."
"Try redeeming your item in a server instead of in DMs."
)
# Handle role mention format
if role_name.startswith('<@&') and role_name.endswith('>'):
role_id = int(role_name.strip('<@&>'))
role = ctx.guild.get_role(role_id)
else:
# Try by name if not a mention
role = discord.utils.get(ctx.guild.roles, name=role_name)
if role is None:
embed = discord.Embed(
title="Role Assignment Failed",
description=f"Could not assign the role `{role_name}` because it does not exist on the server.",
color=discord.Color.red()
)
return await ctx.message.reply(embed=embed)
try:
# Check if user already has the role
if role in ctx.author.roles:
# Ask if they want to remove the role and get a refund
embed = discord.Embed(
title="Role Already Owned",
description=f"You already have the `{role.name}` role. Would you like to remove it and get a refund?",
color=discord.Color.blue()
)
msg = await ctx.message.reply(embed=embed)
# Add reactions for yes/no
await msg.add_reaction("")
await msg.add_reaction("")
def check(reaction, user):
return user == ctx.author and str(reaction.emoji) in ["", ""]
try:
reaction, user = await ctx.bot.wait_for("reaction_add", timeout=30.0, check=check)
if str(reaction.emoji) == "":
await ctx.author.remove_roles(role, reason="Shop role token was refunded.")
# Add the role item back to inventory
async with instance.Inventory() as inv:
if item in inv:
inv[item]["Qty"] += 1
else:
inv[item] = {"Qty": 1, "Type": "role", "Info": f"Grants the {role.name} role", "Role": role_name}
embed = discord.Embed(
title="Role Removed",
description=f"The `{role.name}` role has been removed and refunded to your inventory.",
color=discord.Color.green()
)
await msg.edit(embed=embed)
return
else:
embed = discord.Embed(
title="Action Cancelled",
description="Role removal cancelled.",
color=discord.Color.red()
)
await msg.edit(embed=embed)
return
except asyncio.TimeoutError:
embed = discord.Embed(
title="Timed Out",
description="Role removal timed out.",
color=discord.Color.red()
)
await msg.edit(embed=embed)
return
await ctx.author.add_roles(role, reason="Shop role token was redeemed.")
except discord.Forbidden:
embed = discord.Embed(
title="Role Assignment Failed",
description=(
"The bot could not add this role because it does not have the "
"permission to do so. Make sure the bot has the permissions enabled and "
"its role is higher than the role that needs to be assigned."
),
color=discord.Color.red()
)
await ctx.message.reply(embed=embed)
return False
# Remove the item from inventory
async with instance.Inventory() as inv:
if item in inv:
inv[item]["Qty"] -= 1
if inv[item]["Qty"] <= 0:
del inv[item]
embed = discord.Embed(
title="Role Assigned",
description=f"{ctx.author.display_name} was granted the `{role.name}` role.",
color=discord.Color.green()
)
await ctx.message.reply(embed=embed)
async def pending_prompt(self, ctx, instance, data, item):
"""Handle item redemption with modern UI."""
e = discord.Embed(color=await ctx.embed_colour())
e.add_field(name=item, value=data[item]["Info"], inline=False)
class RedeemView(discord.ui.View):
def __init__(self, cog, timeout=60):
super().__init__(timeout=timeout)
self.cog = cog
self.value = None
@discord.ui.button(label="Redeem", style=discord.ButtonStyle.green)
async def redeem(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != ctx.author:
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
await interaction.response.defer()
self.value = True
self.stop()
@discord.ui.button(label="Cancel", style=discord.ButtonStyle.red)
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
if interaction.user != ctx.author:
return await interaction.response.send_message("This menu is not for you!", ephemeral=True)
await interaction.response.defer()
self.value = False
self.stop()
if data[item]["Type"].lower() == "role":
prompt = f"{ctx.author.mention} Do you wish to redeem {item}? This will grant you the role assigned to this item and it will be removed from your inventory permanently."
else:
prompt = f"{ctx.author.mention} Do you wish to redeem {item}? This will add the item to the pending list for an admin to review and grant. The item will be removed from your inventory while this is processing."
view = RedeemView(self)
msg = await ctx.message.reply(prompt, embed=e, view=view)
try:
await view.wait()
if view.value is None:
await msg.edit(content="Redemption timed out.", view=None)
return
elif not view.value:
await msg.edit(content="Redemption cancelled.", view=None)
return
if data[item]["Type"].lower() == "role":
await self.assign_role(ctx, instance, item, data[item]["Role"])
else:
await self.pending_add(ctx, item)
async with instance.Inventory() as inv:
if item in inv:
inv[item]["Qty"] -= 1
if inv[item]["Qty"] <= 0:
del inv[item]
except Exception as exc:
await msg.edit(content=f"An error occurred: {str(exc)}", view=None)
async def pending_add(self, ctx, item):
"""Add an item to the pending list with modern UI."""
instance = await self.get_instance(ctx, settings=True)
unique_id = str(uuid.uuid4())[:17]
timestamp = ctx.message.created_at.now().strftime("%Y-%m-%d %H:%M:%S")
async with instance.Pending() as p:
if str(ctx.author.id) in p:
p[str(ctx.author.id)][unique_id] = {"Item": item, "Timestamp": timestamp}
else:
p[str(ctx.author.id)] = {unique_id: {"Item": item, "Timestamp": timestamp}}
embed = discord.Embed(
title="Item Pending",
description=f"{ctx.author.mention} added {item} to the pending list.",
color=discord.Color.blue()
)
embed.add_field(name="ID", value=unique_id)
embed.add_field(name="Timestamp", value=timestamp)
if await instance.Settings.Alerts():
alert_role = await instance.Settings.Alert_Role()
role = discord.utils.get(ctx.guild.roles, name=alert_role)
if role:
await ctx.send(role.mention, embed=embed)
else:
await ctx.send(embed=embed)
else:
await ctx.send(embed=embed)
async def change_mode(self, mode):
await self.config.clear_all()
if mode == "global":
await self.config.Global.set(True)
async def shop_is_global(self):
return await self.config.Global()
async def edit_shop(self, ctx, instance):
shops = await instance.Shops.all()
await ctx.send("What shop would you like to edit?")
name = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx, custom=shops).content)
await ctx.send("Would you like to change the shop's `name` or `role` requirement?")
choice = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx, custom=("name", "role")).content)
if choice.content.lower() == "name":
await ctx.send("What is the new name for this shop?")
new_name = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx, length=25).length_under)
async with instance.Shops() as shops:
shops[new_name.content] = shops.pop(name.content)
return await ctx.send("Name changed to {}.".format(new_name.content))
else:
await ctx.send("What is the new role for this shop?")
role = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx).role)
async with instance.Shops() as shops:
shops[name.content]["Role"] = role.content
await ctx.send("{} is now restricted to only users with the {} role.".format(name.content, role.content))
async def delete_shop(self, ctx, instance):
shops = await instance.Shops.all()
await ctx.send("What shop would you like to delete?")
name = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx, custom=shops).content)
await ctx.send("Are you sure you wish to delete {} and all of its items?".format(name.content))
choice = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx).confirm)
if choice.content.lower() == "no":
return await ctx.send("Shop deletion canceled.")
async with instance.Shops() as shops:
del shops[name.content]
await ctx.send("{} was deleted.".format(name.content))
async def create_shop(self, ctx, instance):
await ctx.send("What is the name of this shop?\nName must be 25 characters or less.")
name = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx, length=25).length_under)
if name.content.startswith(ctx.prefix):
return await ctx.send("Closing shop creation. Please don't run commands while attempting to create a shop.")
if name.content in await instance.Shops():
return await ctx.send("A shop with this name already exists.")
msg = (
"What role can use this shop? Use `all` for everyone.\n"
"*Note: this role must exist on this server and is case sensitive.*\n"
)
if await self.shop_is_global():
msg += (
"Shop is also currently in global mode. If you choose to restrict "
"this shop to a role that is on this server, the shop will only be "
"visible on this server to people with the role."
)
await ctx.send(msg)
def predicate(m):
if m.author == ctx.author:
if m.content in [r.name for r in ctx.guild.roles]:
return True
elif m.content.lower() == "all":
return True
else:
return False
else:
return False
try:
role = await ctx.bot.wait_for("message", timeout=25.0, check=predicate)
except asyncio.TimeoutError:
return await ctx.send("Response timed out. Shop creation ended.")
role_name = role.content if role.content != "all" else "@everyone"
async with instance.Shops() as shops:
shops[name.content] = {"Items": {}, "Role": role_name}
await ctx.send(
"Added {} to the list of shops.\n"
"**NOTE:** This shop will not show up until an item is added to it's "
"list.".format(name.content)
)
class ShopManager:
def __init__(self, ctx, instance, user_data):
self.ctx = ctx
self.instance = instance
self.user_data = user_data
@staticmethod
def weighted_choice(choices):
"""Stack Overflow: https://stackoverflow.com/a/4322940/6226473"""
values, weights = zip(*choices)
total = 0
cum_weights = []
for w in weights:
total += w
cum_weights.append(total)
x = random.random() * total
i = bisect(cum_weights, x)
return values[i]
async def random_item(self, shop):
async with self.instance.Shops() as shops:
try:
return self.weighted_choice(
[(x, y["Cost"]) for x, y in shops[shop]["Items"].items() if y["Type"] != "random"]
)
except IndexError:
return
async def auto_handler(self, shop, item, amount):
async with self.instance.Shops() as shops:
msgs = [shops[shop]["Items"][item]["Messages"].pop() for _ in range(amount)]
msg = "\n".join(msgs)
if len(msg) < 2000:
await self.ctx.author.send(msg)
else:
chunks = textwrap.wrap(msg, 2000)
for chunk in chunks:
await asyncio.sleep(2) # At least a little buffer to prevent rate limiting
await self.ctx.author.send(chunk)
async def order(self, shop, item, quantity):
"""Process a purchase order with the specified quantity."""
try:
async with self.instance.Shops() as shops:
if shop not in shops:
return await self.ctx.send(
embed=discord.Embed(
title="Shop Not Found",
description="That shop does not exist.",
color=discord.Color.red()
)
)
if item not in shops[shop]["Items"]:
return await self.ctx.send(
embed=discord.Embed(
title="Item Not Found",
description="That item does not exist in the shop.",
color=discord.Color.red()
)
)
item_data = deepcopy(shops[shop]["Items"][item])
except KeyError:
return await self.ctx.send(
embed=discord.Embed(
title="Error",
description="Could not locate that shop or item.",
color=discord.Color.red()
)
)
# Check if trying to buy multiple role items
if item_data["Type"].lower() == "role":
if quantity > 1:
return await self.ctx.send(
embed=discord.Embed(
title="Invalid Quantity",
description="You can only purchase one copy of a role item.",
color=discord.Color.red()
)
)
# Check if user already has this role item
async with self.user_data.Inventory() as inv:
if item in inv:
return await self.ctx.send(
embed=discord.Embed(
title="Already Owned",
description="You already own this role item. You can only have one copy at a time.",
color=discord.Color.red()
)
)
# Validate quantity
if quantity is None:
return await self.ctx.send(
embed=discord.Embed(
title="Invalid Quantity",
description="No quantity specified for purchase.",
color=discord.Color.red()
)
)
cur = await bank.get_currency_name(self.ctx.guild)
stock, cost, _type = item_data["Qty"], item_data["Cost"], item_data["Type"]
# Check if item is in stock
if stock != "--" and stock <= 0:
return await self.ctx.send(
embed=discord.Embed(
title="Out of Stock",
description=f"Sorry, {item} is out of stock.",
color=discord.Color.red()
)
)
# Validate quantity for random items
if _type == "random" and quantity != 1:
return await self.ctx.send(
embed=discord.Embed(
title="Invalid Quantity",
description="You can only buy 1 random item at a time.",
color=discord.Color.red()
)
)
# Check if enough stock
if stock != "--" and quantity > stock:
return await self.ctx.send(
embed=discord.Embed(
title="Insufficient Stock",
description=f"Not enough stock! Only {stock} available.",
color=discord.Color.red()
)
)
total_cost = cost * quantity
try:
await bank.withdraw_credits(self.ctx.author, total_cost)
except ValueError:
return await self.ctx.send(
embed=discord.Embed(
title="Insufficient Funds",
description=f"You cannot afford {quantity}x {item} for {total_cost} {cur}.",
color=discord.Color.red()
)
)
# Handle different item types
if _type == "auto":
await self.auto_handler(shop, item, quantity)
await self.remove_stock(shop, item, stock, quantity)
return await self.ctx.send(
embed=discord.Embed(
title="Auto Item Delivered",
description="Message sent.",
color=discord.Color.green()
)
)
if _type == "random":
new_item = await self.random_item(shop)
if new_item is None:
try:
await bank.deposit_credits(self.ctx.author, total_cost)
except BalanceTooHigh as e:
await bank.set_balance(self.ctx.author, e.max_balance)
return await self.ctx.send(
embed=discord.Embed(
title="No Items Available",
description=f"There aren't any non-random items available in {shop}, so {item} cannot be purchased.",
color=discord.Color.red()
)
)
else:
await self.remove_stock(shop, item, stock, quantity)
item = new_item
async with self.instance.Shops() as shops:
item_data = deepcopy(shops[shop]["Items"][new_item])
stock = item_data["Qty"]
# Update stock and add to inventory
await self.remove_stock(shop, item, stock, quantity)
success = await self.add_to_inventory(item, item_data, quantity)
if success:
await self.ctx.send(
embed=discord.Embed(
title="Purchase Successful",
description=f"{self.ctx.author.mention} purchased {quantity}x {item} for {total_cost} {cur}.",
color=discord.Color.green()
)
)
else:
# Refund if inventory add failed (e.g. already had role item)
try:
await bank.deposit_credits(self.ctx.author, total_cost)
except BalanceTooHigh as e:
await bank.set_balance(self.ctx.author, e.max_balance)
await self.ctx.send(
embed=discord.Embed(
title="Purchase Failed",
description="You already own this item. Purchase has been refunded.",
color=discord.Color.red()
)
)
async def remove_stock(self, shop, item, current_stock, amount):
"""Safely remove items from stock."""
if current_stock == "--":
return
async with self.instance.Shops() as shops:
new_stock = current_stock - amount
if new_stock > 0:
shops[shop]["Items"][item]["Qty"] = new_stock
else:
del shops[shop]["Items"][item]
async def add_to_inventory(self, item, item_data, quantity):
"""Safely add items to user inventory."""
async with self.user_data.Inventory() as inv:
# For role items, only allow one copy
if item_data["Type"].lower() == "role":
if item in inv:
return False # Already has the role item
inv[item] = deepcopy(item_data)
inv[item]["Qty"] = 1 # Force quantity to 1 for role items
return True
else:
if item in inv:
inv[item]["Qty"] += quantity
else:
inv[item] = deepcopy(item_data)
inv[item]["Qty"] = quantity
return True
async def remove(self, item, number=1):
"""Remove an item from user's inventory."""
async with self.user_data.Inventory() as inv:
if item in inv:
inv[item]["Qty"] -= number
if inv[item]["Qty"] <= 0:
del inv[item]
class ItemManager:
def __init__(self, ctx, instance):
self.ctx = ctx
self.instance = instance
async def run(self, action):
if action.lower() == "create":
await self.create()
elif action.lower() == "edit":
await self.edit()
else:
await self.delete()
async def create(self):
name = await self.set_name()
if not name:
return
cost = await self.set_cost()
info = await self.set_info()
_type, role, msgs = await self.set_type()
if _type != "auto":
qty = await self.set_quantity(_type)
else:
qty = len(msgs)
data = {
"Cost": cost,
"Qty": qty,
"Type": _type,
"Info": info,
"Role": role,
"Messages": msgs,
}
msg = "What shop would you like to add this item to?\n"
shops = await self.instance.Shops()
if shops:
msg += "Current shops are: "
msg += humanize_list([f"`{shopname}`" for shopname in sorted(shops.keys())])
msg += "\nYou can also enter a new shop name to create a new shop."
else:
msg += "No shops exist yet. Enter a name for your new shop."
await self.ctx.send(msg)
# Remove the check for existing shops to allow new shop creation
shop = await self.ctx.bot.wait_for("message", timeout=25, check=Checks(self.ctx, length=25).length_under)
if shop.content not in shops:
# Create new shop with @everyone role
async with self.instance.Shops() as shops:
shops[shop.content] = {"Items": {}, "Role": "@everyone"}
await self.ctx.send(f"Created new shop: {shop.content}")
await self.add(data, shop.content, name, new_allowed=True)
await self.ctx.send("Item creation complete.")
async def delete(self):
shop_list = await self.instance.Shops.all()
def predicate(m):
if self.ctx.author != m.author:
return False
return m.content in shop_list
await self.ctx.send("What shop would you like to delete an item from?")
shop = await self.ctx.bot.wait_for("message", timeout=25, check=predicate)
def predicate2(m):
if self.ctx.author != m.author:
return False
return m.content in shop_list[shop.content]["Items"]
await self.ctx.send("What item would you like to delete from this shop?")
item = await self.ctx.bot.wait_for("message", timeout=25, check=predicate2)
await self.ctx.send("Are you sure you want to delete {} from {}?".format(item.content, shop.content))
choice = await self.ctx.bot.wait_for("message", timeout=25, check=Checks(self.ctx).confirm)
if choice.content.lower() == "yes":
async with self.instance.Shops() as shops:
del shops[shop.content]["Items"][item.content]
await self.ctx.send("{} was deleted from the {}.".format(item.content, shop.content))
else:
await self.ctx.send("Item deletion canceled.")
async def edit(self):
choices = ("name", "type", "role", "qty", "cost", "msgs", "quantity", "info", "messages")
while True:
shop, item, item_data = await self.get_item()
def predicate(m):
if self.ctx.author != m.author:
return False
if m.content.lower() in ("msgs", "messages") and item_data["Type"] != "auto":
return False
elif m.content.lower() in ("qty", "quantity") and item_data["Type"] == "auto":
return False
elif m.content.lower() == "role" and item_data["Type"] != "role":
return False
elif m.content.lower() not in choices:
return False
else:
return True
await self.ctx.send(
"What would you like to edit for this item?\n"
"`Name`, `Type`, `Role`, `Quantity`, `Cost`, `Info`, or `Messages`?\n"
"Note that `Messages` cannot be edited on non-auto type items."
)
choice = await self.ctx.bot.wait_for("message", timeout=25.0, check=predicate)
if choice.content.lower() == "name":
await self.set_name(item=item, shop=shop)
elif choice.content.lower() == "type":
await self.set_type(item, shop)
elif choice.content.lower() == "role":
await self.set_role(item, shop)
elif choice.content.lower() in ("qty", "quantity"):
await self.set_quantity(item_data["Type"], item, shop)
elif choice.content.lower() == "cost":
await self.set_cost(item=item, shop=shop)
elif choice.content.lower() == "info":
await self.set_info(item=item, shop=shop)
else:
await self.set_messages(item_data["Type"], item=item, shop=shop)
await self.ctx.send("Would you like to continue editing?")
rsp = await self.ctx.bot.wait_for("message", timeout=25, check=Checks(self.ctx).confirm)
if rsp.content.lower() == "no":
await self.ctx.send("Editing process exited.")
break
async def set_messages(self, _type, item=None, shop=None):
if _type != "auto":
return await self.ctx.send("You can only add messages to auto type items.")
await self.ctx.send(
"Auto items require a message to be stored per quantity. Separate each "
"message with a new line using a code block."
)
msgs = await self.ctx.bot.wait_for("message", timeout=120, check=Checks(self.ctx).same)
auto_msgs = [x.strip() for x in msgs.content.strip("`").split("\n") if x]
if item:
async with self.instance.Shops() as shops:
shops[shop]["Items"][item]["Messages"].extend(auto_msgs)
shops[shop]["Items"][item]["Qty"] += len(auto_msgs)
return await self.ctx.send("{} messages were added to {}.".format(len(auto_msgs), item))
return auto_msgs
async def set_name(self, item=None, shop=None):
await self.ctx.send("Enter a name for this item. It can't be longer than 20 characters.")
name = await self.ctx.bot.wait_for("message", timeout=25, check=Checks(self.ctx, length=30).length_under)
if item:
async with self.instance.Shops() as shops:
shops[shop]["Items"][name.content] = shops[shop]["Items"].pop(item)
return await self.ctx.send("{}'s name was changed to {}.".format(item, name.content))
return name.content
async def set_cost(self, item=None, shop=None):
await self.ctx.send("Enter the new cost for this item.")
cost = await self.ctx.bot.wait_for("message", timeout=25, check=Checks(self.ctx).positive)
if item:
async with self.instance.Shops() as shops:
shops[shop]["Items"][item]["Cost"] = int(cost.content)
return await self.ctx.send("This item now costs {}.".format(cost.content))
return int(cost.content)
def hierarchy_check(self, m):
# Extract role ID from mention if it's a mention
if m.role_mentions:
role = m.role_mentions[0]
if self.ctx.author == m.author:
if self.ctx.author.top_role >= role:
return True
return False
# Regular role name check
roles = [r.name for r in self.ctx.guild.roles if r.name != "Bot"]
if self.ctx.author == m.author and m.content in roles:
if self.ctx.author.top_role >= discord.utils.get(self.ctx.message.guild.roles, name=m.content):
return True
return False
async def set_role(self, item=None, shop=None):
await self.ctx.send(
"What role do you wish for this item to assign?\n*Note, you cannot add a role higher than your own.*"
)
role = await self.ctx.bot.wait_for("message", timeout=25, check=self.hierarchy_check)
if item:
async with self.instance.Shops() as shops:
shops[shop]["Items"][item]["Role"] = role.content
return await self.ctx.send("This item now assigns the {} role.".format(role.content))
return role.content
async def set_quantity(self, _type=None, item=None, shop=None):
if _type == "auto":
return await self.ctx.send(
"You can't change the quantity of an auto item. The quantity will match the messages set."
)
await self.ctx.send("What quantity do you want to set this item to?\nType 0 for infinite.")
def check(m):
return m.author == self.ctx.author and m.content.isdigit() and int(m.content) >= 0
qty = await self.ctx.bot.wait_for("message", timeout=25, check=check)
qty = int(qty.content) if int(qty.content) > 0 else "--"
if item:
async with self.instance.Shops() as shops:
shops[shop]["Items"][item]["Qty"] = qty
return await self.ctx.send(
"Quantity for {} now set to {}.".format(item, "infinite." if qty == "--" else qty)
)
return qty
async def set_type(self, item=None, shop=None):
valid_types = ("basic", "random", "auto", "role")
await self.ctx.send(
"What is the item type?\n"
"```\n"
"basic - Normal item and is added to the pending list when redeemed.\n"
"random - Picks a random item in the shop, weighted on cost.\n"
"role - Grants a role when redeemed.\n"
"auto - DM's a msg to the user instead of adding to their inventory.\n"
"```"
)
_type = await self.ctx.bot.wait_for("message", timeout=25, check=Checks(self.ctx, custom=valid_types).content)
if _type.content.lower() == "auto":
msgs = await self.set_messages("auto", item=item, shop=shop)
if not item:
return "auto", None, msgs
elif _type.content.lower() == "role":
role = await self.set_role(item=item, shop=shop)
if not item:
return "role", role, None
else:
if item:
async with self.instance.Shops() as shops:
shops[shop]["Items"][item]["Type"] = _type.content.lower()
try:
del shops[shop]["Items"][item]["Messages"]
del shops[shop]["Items"][item]["Role"]
except KeyError:
pass
return await self.ctx.send("Item type set to {}.".format(_type.content.lower()))
return _type.content.lower(), None, None
async with self.instance.Shops() as shops:
shops[shop]["Items"][item]["Type"] = _type.content.lower()
async def set_info(self, item=None, shop=None):
await self.ctx.send("Specify the info text for this item.\n*Note* cannot be longer than 500 characters.")
info = await self.ctx.bot.wait_for("message", timeout=40, check=Checks(self.ctx, length=500).length_under)
if item:
async with self.instance.Shops() as shops:
shops[shop]["Items"][item]["Info"] = info.content
return await self.ctx.send("Info now set to:\n{}".format(info.content))
return info.content
async def get_item(self):
shops = await self.instance.Shops.all()
await self.ctx.send("What shop is the item you would like to edit in?")
shop = await self.ctx.bot.wait_for("message", timeout=25.0, check=Checks(self.ctx, custom=shops).content)
items = shops[shop.content]["Items"]
await self.ctx.send("What item would you like to edit?")
item = await self.ctx.bot.wait_for("message", timeout=25.0, check=Checks(self.ctx, custom=items).content)
return shop.content, item.content, shops[shop.content]["Items"][item.content]
async def add(self, data, shop, item, new_allowed=False):
async with self.instance.Shops() as shops:
if shop not in shops:
if new_allowed:
shops[shop] = {"Items": {item: data}, "Role": "@everyone"}
return log.info("Created the shop: {} and added {}.".format(shop, item))
log.error("{} could not be added to {}, because it does not exist.".format(item, shop))
elif item in shops[shop]["Items"]:
log.error("{} was not added because that item already exists in {}.".format(item, shop))
else:
shops[shop]["Items"][item] = data
log.info("{} added to {}.".format(item, shop))
async def remove(self, shop, item, stock, amount):
try:
remainder = stock - amount
async with self.instance.Shops() as shops:
if remainder > 0:
shops[shop]["Items"][item]["Qty"] = remainder
else:
del shops[shop]["Items"][item]
except TypeError:
pass
return
class Parser:
def __init__(self, ctx, instance, msg):
self.ctx = ctx
self.instance = instance
self.msg = msg
self.is_init = ctx is None # Flag to indicate if this is initialization
@staticmethod
def basic_checks(idx, row):
if len(row["Shop"]) > 25:
log.warning("Row {} was not added because shop name was too long.".format(idx))
return False
elif len(row["Item"]) > 30:
log.warning("Row {} was not added because item name was too long.".format(idx))
return False
elif not row["Cost"].isdigit() or int(row["Cost"]) < 0:
log.warning("Row {} was not added because the cost was lower than 0.".format(idx))
return False
elif not row["Qty"].isdigit() or int(row["Qty"]) < 0:
log.warning("Row {} was not added because the quantity was lower than 0.".format(idx))
return False
elif len(row["Info"]) > 500:
log.warning("Row {} was not added because the info was too long.".format(idx))
return False
else:
return True
def type_checks(self, idx, row, messages):
if row["Type"].lower() not in ("basic", "random", "auto", "role"):
log.warning("Row {} was not added because of an invalid type.".format(idx))
return False
elif row["Type"].lower() == "role" and not row["Role"]:
log.warning("Row {} was not added because the type is a role, but no role was set.".format(idx))
return False
elif not self.is_init: # Only do role checks if not in initialization
if row["Type"].lower() == "role" and discord.utils.get(self.ctx.message.guild.roles, name=row["Role"]) is None:
log.warning(
"Row {} was not added because the {} role does not exist on the server.".format(idx, row["Role"])
)
return False
elif row["Type"].lower() == "role":
if discord.utils.get(self.ctx.message.guild.roles, name=row["Role"]) > self.ctx.author.top_role:
log.warning(
"Row {} was not added because the {} role is higher than the "
"shopkeeper's highest role.".format(idx, row["Role"])
)
return False
if row["Type"].lower() == "auto" and int(row["Qty"]) == 0:
log.warning("Row {} was not added because auto items cannot have an infinite quantity.".format(idx))
return False
elif row["Type"].lower() == "auto" and int(row["Qty"]) != len(messages):
log.warning(
"Row {} was not added because auto items must have an equal number of "
"messages and quantity.".format(idx)
)
return False
elif row["Type"].lower() == "auto" and any(len(x) > 2000 for x in messages):
log.warning("Row {} was not added because one of the messages exceeds 2000 characters.".format(idx))
return False
return True
async def parse_text_entry(self, text):
keys = ("Shop", "Item", "Type", "Qty", "Cost", "Info", "Role", "Messages")
raw_data = [
[f.strip() for f in y] for y in [x.split(",") for x in text.strip("`").split("\n") if x] if 6 <= len(y) <= 8
]
bulk = [{key: value for key, value in zip_longest(keys, x)} for x in raw_data]
await self.parse_bulk(bulk)
async def search_csv(self, file_path):
try:
with file_path.open("rt") as f:
reader = csv.DictReader(f, delimiter=",")
await self.parse_bulk(reader)
except FileNotFoundError:
await self.msg.edit(content="The specified filename could not be found.")
async def parse_bulk(self, reader):
if not reader:
return await self.msg.edit(content="Data was faulty. No data was added.")
keys = ("Cost", "Qty", "Type", "Info", "Role", "Messages")
for idx, row in enumerate(reader, 1):
try:
messages = [x.strip() for x in row["Messages"].split(",") if x]
except AttributeError:
messages = []
if not self.basic_checks(idx, row):
continue
elif not self.type_checks(idx, row, messages):
continue
else:
data = {
key: row.get(key, None)
if key not in ("Cost", "Qty", "Messages")
else int(row[key])
if key != "Messages"
else messages
for key in keys
}
if data["Qty"] == 0:
data["Qty"] = "--"
item_manager = ItemManager(self.ctx, self.instance)
await item_manager.add(data, row["Shop"], row["Item"], new_allowed=True)
await self.msg.edit(content="Bulk process finished. Please check your console for more information.")
class ExitProcess(Exception):
pass