2016 lines
85 KiB
Python
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
|