Ruby-Cogs/shop/shop.py

1423 lines
58 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 .menu import ShopMenu
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.1.13"
__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)
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()
@commands.max_concurrency(1, commands.BucketType.user)
async def inventory(self, ctx):
"""Displays your purchased items."""
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.")
if not await instance.Inventory():
return await ctx.send("You don't have any items to display.")
data = await instance.Inventory.all()
menu = Inventory(ctx, list(data.items()))
try:
item = await menu.display()
except RuntimeError:
return
await self.pending_prompt(ctx, instance, data, item)
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()
@commands.max_concurrency(1, commands.BucketType.user)
async def buy(self, ctx, *purchase):
"""Shop menu appears with no purchase order.
When no argument is specified for purchase, it will bring up the
shop menu.
Using the purchase argument allows direct purchases from a shop.
The order is "Shop Name" "Item Name" and names with spaces
must include quotes.
Examples
--------
[p]shop buy \"Secret Shop\" oil
[p]shop buy Junkyard tire
[p]shop buy \"Holy Temple\" \"Healing Potion\"
"""
try:
instance = await self.get_instance(ctx, settings=True)
except AttributeError:
return await ctx.send("You can't use this command in DMs when not in global mode.")
if not await instance.Shops():
return await ctx.send("No shops have been created yet.")
if await instance.Settings.Closed():
return await ctx.send("The shop system is currently closed.")
shops = await instance.Shops.all()
col = await self.check_availability(ctx, shops)
if not col:
return await ctx.send(
"Either no items have been created, you need a higher role, "
"or this command should be used in a server and not DMs."
)
if purchase:
try:
shop, item = purchase
except ValueError:
return await ctx.send("Too many parameters passed. Use help on this command for more information.")
if shop not in shops:
return await ctx.send("Either that shop does not exist, or you don't have access to it.")
else:
style = await instance.Settings.Sorting()
menu = ShopMenu(ctx, shops, sorting=style)
try:
shop, item = await menu.display()
except RuntimeError:
return
user_data = await self.get_instance(ctx, user=ctx.author)
sm = ShopManager(ctx, instance, user_data)
try:
await sm.order(shop, item)
except asyncio.TimeoutError:
await ctx.send("Request timed out.")
except ExitProcess:
await ctx.send("Transaction canceled.")
@commands.max_concurrency(1, commands.BucketType.user)
@shop.command()
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.send("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.send("Your inventory is empty.")
if item not in data:
return await ctx.send("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.
Cooldown is a static 60 seconds to prevent abuse.
Cooldown will trigger regardless of the outcome.
"""
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.send("This user has trading turned off.")
if item not in author_inventory:
return await ctx.send("You don't own that item.")
if 0 < author_inventory[item]["Qty"] < quantity:
return await ctx.send("You don't have that many {}".format(item))
await ctx.send(
"{} 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.send("Trade request timed out. Canceled trade.")
if decision.content.lower() in ("no", cancel):
return await ctx.send("Trade canceled.")
await ctx.send("{} 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.send("Trade request timed out. Canceled trade.")
if offer.content.lower() == cancel:
return await ctx.send("Trade canceled.")
qty, item2 = [x.strip() for x in offer.content.split('"')[:2] if x]
await ctx.send(
"{} 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.send("Trade request timed out. Canceled trade.")
if final.content.lower() in ("no", cancel):
return await ctx.send("Trade canceled.")
sm1 = ShopManager(ctx, instance=None, user_data=author_instance)
await sm1.add(item2, user_inv[item2], int(qty))
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.send("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 version(self, ctx):
"""Shows the current Shop version."""
await ctx.send("Shop is running version {}.".format(__version__))
@shop.command()
@commands.is_owner()
async def wipe(self, ctx):
"""Wipes all shop cog data."""
await ctx.send(
"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.send("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.send(msg)
else:
return await ctx.send("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()
menu = ShopMenu(ctx, data, mode=1, sorting="name")
try:
user, item, = await menu.display()
except RuntimeError:
return
try:
await self.clear_single_pending(ctx, instance, data, item, user)
except asyncio.TimeoutError:
await ctx.send("Request timed out.")
@shop.command()
@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.
The item must be in your inventory and have enough to cover the quantity.
Examples
--------
[p]shop gift Redjumpman 3 Healing Potion
[p]shop give @Navi 1 Demon Sword
"""
if quantity < 1:
return await ctx.send(":facepalm: How would that work genius?")
if user == ctx.author:
return await ctx.send("Really? Maybe you should find some friends.")
settings = await self.get_instance(ctx, settings=True)
if not await settings.Settings.Gifting():
return await ctx.send("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.send(f"You don't own any `{item}`.")
if author_inv[item]["Qty"] < quantity:
return await ctx.send(f"You don't have that many `{item}` to give.")
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)
await sm2.add(item, author_inv[item], quantity)
await ctx.send(f"{ctx.author.mention} gifted {user.mention} {quantity}x {item}.")
@shop.command()
@global_permissions()
@commands.guild_only()
async def give(self, ctx, user: discord.Member, quantity: int, *shopitem):
"""Administratively gives a user an item.
Shopitem argument must be a \"Shop Name\" \"Item Name\" format.
The item must be in the shop in order for this item to be given.
Only basic and role items can be given.
Giving a user an item does not affect the stock in the shop.
Examples
--------
[p]shop give Redjumpman 1 "Holy Temple" "Healing Potion"
[p]shop give Redjumpman 1 Junkyard Scrap
"""
if quantity < 1:
return await ctx.send(":facepalm: You can't do that.")
if shopitem is None:
return await ctx.send_help()
try:
shop, item = shopitem
except ValueError:
return await ctx.send('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.send("Invalid shop name.")
elif item not in shops[shop]["Items"]:
return await ctx.send("That item in not in the {} shop.".format(shop))
elif shops[shop]["Items"][item]["Type"] not in ("basic", "role"):
return await ctx.send("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.send("{} 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.send("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.send("Canceled inventory wipe.")
instance = await self.get_instance(ctx=ctx, user=user)
await instance.Inventory.clear()
await ctx.send("Done. Inventory wiped for {}.".format(user.name))
@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):
if await self.config.Global():
if not ctx.guild:
return await ctx.send(
"Unable to assign role, because shop is in global mode."
"Try redeeming your item in a server instead of in DMs."
)
role = discord.utils.get(ctx.message.guild.roles, name=role_name)
if role is None:
return await ctx.send(
"Could not assign the role, {}, because it does not exist on the server.".format(role_name)
)
try:
await ctx.author.add_roles(role, reason="Shop role token was redeemed.")
except discord.Forbidden:
await ctx.send(
"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."
)
return False
sm = ShopManager(ctx, None, instance)
await sm.remove(item)
await ctx.send("{} was granted the {} role.".format(ctx.author.mention, role.name))
async def pending_add(self, ctx, item):
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}}
msg = "{} added {} to your pending list.".format(ctx.author.mention, item)
if await instance.Settings.Alerts():
alert_role = await instance.Settings.Alert_Role()
role = discord.utils.get(ctx.guild.roles, name=alert_role)
if role:
msg = "{}\n{}".format(role.mention, msg)
await ctx.send(msg)
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)
)
async def pending_prompt(self, ctx, instance, data, item):
e = discord.Embed(color=await ctx.embed_colour())
e.add_field(name=item, value=data[item]["Info"], inline=False)
if data[item]["Type"].lower() == "Role":
await ctx.send(
"{} Do you wish to redeem {}? This will grant you the role assigned to "
"this item and it will be removed from your inventory "
"permanently.".format(ctx.author.mention, item),
embed=e,
)
else:
await ctx.send(
"{} Do you wish to redeem {}? 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.".format(ctx.author.mention, item),
embed=e,
)
try:
choice = await ctx.bot.wait_for("message", timeout=25, check=Checks(ctx).confirm)
except asyncio.TimeoutError:
return await ctx.send("No Response. Item redemption canceled.")
if choice.content.lower() != "yes":
return await ctx.send("Canceled item redemption.")
if data[item]["Type"].lower() == "role":
return await self.assign_role(ctx, instance, item, data[item]["Role"])
else:
await self.pending_add(ctx, item)
sm = ShopManager(ctx, instance=None, user_data=instance)
await sm.remove(item)
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):
try:
async with self.instance.Shops() as shops:
item_data = deepcopy(shops[shop]["Items"][item])
except KeyError:
return await self.ctx.send("Could not locate that shop or item.")
cur = await bank.get_currency_name(self.ctx.guild)
stock, cost, _type = item_data["Qty"], item_data["Cost"], item_data["Type"]
e = discord.Embed(color=await self.ctx.embed_colour())
e.add_field(name=item, value=item_data["Info"], inline=False)
text = (
f"How many {item} would you like to purchase?\n*If this "
f"is a random item, you can only buy 1 at a time.*"
)
await self.ctx.send(content=text, embed=e)
def predicate(m):
if m.author == self.ctx.author and self.ctx.channel == m.channel:
if m.content.isdigit():
if _type == "random":
return int(m.content) == 1
try:
return 0 < int(m.content) <= stock
except TypeError:
return 0 < int(m.content)
elif m.content.lower() in ("exit", "cancel", "e", "x"):
return True
else:
return False
num = await self.ctx.bot.wait_for("message", timeout=25.0, check=predicate)
if num.content.lower() in ("exit", "cancel", "e", "x"):
raise ExitProcess()
amount = int(num.content)
try:
await num.delete()
except (discord.NotFound, discord.Forbidden):
pass
cost *= amount
try:
await bank.withdraw_credits(self.ctx.author, cost)
except ValueError:
return await self.ctx.send(
"You cannot afford {}x {} for {} {}. Transaction ended.".format(num.content, item, cost, cur)
)
im = ItemManager(self.ctx, self.instance)
if _type == "auto":
await self.auto_handler(shop, item, amount)
await im.remove(shop, item, stock, amount)
return await self.ctx.send("Message sent.")
if _type == "random":
new_item = await self.random_item(shop)
if new_item is None:
try:
await bank.deposit_credits(self.ctx.author, cost)
except BalanceTooHigh as e:
await bank.set_balance(self.ctx.author, e.max_balance)
return await self.ctx.send(
"There aren't any non-random items available in {}, "
"so {} cannot be purchased.".format(shop, item)
)
else:
await im.remove(shop, item, stock, amount)
item = new_item
async with self.instance.Shops() as shops:
item_data = deepcopy(shops[shop]["Items"][new_item])
stock = item_data["Qty"]
await im.remove(shop, item, stock, amount)
await self.add(item, item_data, amount)
await self.ctx.send("{} purchased {}x {} for {} {}.".format(self.ctx.author.mention, amount, item, cost, cur))
async def add(self, item, data, quantity):
async with self.user_data.Inventory() as inv:
if item in inv:
inv[item]["Qty"] += quantity
else:
inv[item] = data
inv[item]["Qty"] = quantity
async def remove(self, item, number=1):
async with self.user_data.Inventory() as inv:
if number >= inv[item]["Qty"]:
del inv[item]
else:
inv[item]["Qty"] -= number
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()
msg += "Current shops are: "
msg += humanize_list([f"`{shopname}`" for shopname in sorted(shops.keys())])
await self.ctx.send(msg)
shop = await self.ctx.bot.wait_for("message", timeout=25, check=Checks(self.ctx, custom=shops.keys()).content)
await self.add(data, shop.content, name)
await self.ctx.send("Item creation complete. Check your logs to ensure it went to the approriate shop.")
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 name.content.startswith(self.ctx.prefix):
await self.ctx.send("Closing item creation. Please don't run commands while attempting to create an item.")
return None
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):
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
else:
return False
else:
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
@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 (
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() == "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
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
else:
return True
else:
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