rewrite: Begin shop module.
This commit is contained in:
8
bot/modules/shop/__init__.py
Normal file
8
bot/modules/shop/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from babel import LocalBabel
|
||||||
|
|
||||||
|
babel = LocalBabel('shop')
|
||||||
|
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
from .cog import Shopping
|
||||||
|
await bot.add_cog(Shopping(bot))
|
||||||
109
bot/modules/shop/cog.py
Normal file
109
bot/modules/shop/cog.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands as cmds
|
||||||
|
from discord import app_commands as appcmds
|
||||||
|
|
||||||
|
from meta import LionBot, LionCog, LionContext
|
||||||
|
|
||||||
|
from utils import ui
|
||||||
|
|
||||||
|
from . import babel
|
||||||
|
|
||||||
|
from .shops.colours import ColourShop
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class Shopping(LionCog):
|
||||||
|
Shops = [ColourShop]
|
||||||
|
|
||||||
|
def __init__(self, bot: LionBot):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = None
|
||||||
|
self.shops = []
|
||||||
|
|
||||||
|
async def cog_load(self):
|
||||||
|
for Shop in self.Shops:
|
||||||
|
shop = Shop(self.bot, self.data)
|
||||||
|
await shop.load_into(self)
|
||||||
|
self.shops.append(shop)
|
||||||
|
|
||||||
|
async def cog_unload(self):
|
||||||
|
for shop in self.shops:
|
||||||
|
await shop.unload()
|
||||||
|
|
||||||
|
@cmds.hybrid_group(name='editshop')
|
||||||
|
async def editshop_group(self, ctx: LionContext):
|
||||||
|
return
|
||||||
|
|
||||||
|
@cmds.hybrid_group(name='shop')
|
||||||
|
async def shop_group(self, ctx: LionContext):
|
||||||
|
return
|
||||||
|
|
||||||
|
@shop_group.command(name='open')
|
||||||
|
async def shop_open_cmd(self, ctx: LionContext):
|
||||||
|
"""
|
||||||
|
Opens the shop UI for the current guild.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class StoreManager(ui.LeoUI):
|
||||||
|
def __init__(self, bot, data, shops):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = data
|
||||||
|
self.shops = shops
|
||||||
|
|
||||||
|
self.page_num = 0
|
||||||
|
|
||||||
|
self._original: Optional[discord.Interaction] = None
|
||||||
|
|
||||||
|
self._store_row = self.make_buttons()
|
||||||
|
self._widgets = self.prepare_widgets()
|
||||||
|
|
||||||
|
async def redraw(self):
|
||||||
|
"""
|
||||||
|
Ask the current shop widget to redraw.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def make_buttons(self):
|
||||||
|
"""
|
||||||
|
Make a tuple of shop buttons.
|
||||||
|
|
||||||
|
If there is only one shop, return an empty tuple.
|
||||||
|
"""
|
||||||
|
if len(self.shops) <= 1:
|
||||||
|
return ()
|
||||||
|
buttons = []
|
||||||
|
for i, shop in enumerate(self.shops):
|
||||||
|
@ui.AButton(label=shop.name)
|
||||||
|
async def pressed_switch_shop(press: discord.Interaction, pressed):
|
||||||
|
await press.response.defer()
|
||||||
|
await self.change_page(i)
|
||||||
|
buttons.append(pressed_switch_shop)
|
||||||
|
return tuple(buttons)
|
||||||
|
|
||||||
|
def prepare_widgets(self):
|
||||||
|
widgets = []
|
||||||
|
for shop in self.shops:
|
||||||
|
widget = shop.make_widget()
|
||||||
|
# TODO: Update this when we have a UILayout class
|
||||||
|
# widget.layout.set_row('shops', self._store_row, affinity=1)
|
||||||
|
widget.shop_row = self._store_row
|
||||||
|
widgets.append(widget)
|
||||||
|
return widgets
|
||||||
|
|
||||||
|
async def change_page(self, i):
|
||||||
|
"""
|
||||||
|
Change to the given page number.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
async def run(self, interaction):
|
||||||
|
...
|
||||||
87
bot/modules/shop/data.py
Normal file
87
bot/modules/shop/data.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
from enums import Enum
|
||||||
|
from cachetools import TTLCache
|
||||||
|
|
||||||
|
|
||||||
|
from data import Registry, RowModel, RegisterEnum, WeakCache
|
||||||
|
from data.columns import Integer, String, Bool, Timestamp, Column
|
||||||
|
|
||||||
|
|
||||||
|
class ShopItemType(Enum):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TYPE ShopItemType AS ENUM (
|
||||||
|
'COLOUR_ROLE'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
COLOUR = 'COLOUR_ROLE',
|
||||||
|
|
||||||
|
|
||||||
|
class ShopData(Registry, name='shop'):
|
||||||
|
_ShopItemType = RegisterEnum(ShopItemType)
|
||||||
|
|
||||||
|
class ShopItem(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE shop_items(
|
||||||
|
itemid SERIAL PRIMARY KEY,
|
||||||
|
guildid BIGINT NOT NULL,
|
||||||
|
item_type ShopItemType NOT NULL,
|
||||||
|
price INTEGER NOT NULL,
|
||||||
|
purchasable BOOLEAN DEFAULT TRUE,
|
||||||
|
deleted BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT (now() at time zone 'utc')
|
||||||
|
);
|
||||||
|
CREATE INDEX guild_shop_items ON shop_items (guildid);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'shop_items'
|
||||||
|
|
||||||
|
itemid = Integer(primary=True)
|
||||||
|
guildid = Integer()
|
||||||
|
item_type: Column[ShopItemType] = Column()
|
||||||
|
price = Integer()
|
||||||
|
purchasable = Bool()
|
||||||
|
deleted = Bool()
|
||||||
|
created_at = Timestamp()
|
||||||
|
|
||||||
|
class ColourRole(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE shop_items_colour_roles(
|
||||||
|
itemid INTEGER PRIMARY KEY REFERENCES shop_items(itemid) ON DELETE CASCADE,
|
||||||
|
roleid BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'shop_items_colour_roles'
|
||||||
|
|
||||||
|
itemid = Integer(primary=True)
|
||||||
|
roleid = Integer()
|
||||||
|
|
||||||
|
class MemberInventory(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE member_inventory(
|
||||||
|
inventoryid SERIAL PRIMARY KEY,
|
||||||
|
guildid BIGINT NOT NULL,
|
||||||
|
userid BIGINT NOT NULL,
|
||||||
|
transactionid INTEGER REFERENCES coin_transactions(transactionid) ON DELETE SET NULL,
|
||||||
|
itemid INTEGER NOT NULL REFERENCES shop_items(itemid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX member_inventory_members ON member_inventory(guildid, userid);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'member_inventory'
|
||||||
|
|
||||||
|
inventoryid = Integer(primary=True)
|
||||||
|
guildid = Integer()
|
||||||
|
userid = Integer()
|
||||||
|
transactionid = Integer()
|
||||||
|
itemid = Integer()
|
||||||
|
|
||||||
|
async def fetch_inventory(self, guildid, userid) -> list['ShopData.MemberInventory']:
|
||||||
|
"""
|
||||||
|
Fetch the given member's inventory.
|
||||||
|
"""
|
||||||
|
return await self.fetch_where(guildid=guildid, userid=userid)
|
||||||
0
bot/modules/shop/shops/__init__.py
Normal file
0
bot/modules/shop/shops/__init__.py
Normal file
81
bot/modules/shop/shops/base.py
Normal file
81
bot/modules/shop/shops/base.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ui.button import Button
|
||||||
|
|
||||||
|
from meta import LionBot
|
||||||
|
|
||||||
|
from utils import ui
|
||||||
|
|
||||||
|
from ..data import ShopData
|
||||||
|
|
||||||
|
|
||||||
|
class MemberInventory:
|
||||||
|
"""
|
||||||
|
An interface to the member's inventory.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot, shop_data, lion, inventory):
|
||||||
|
self.bot = bot
|
||||||
|
self.lion = lion
|
||||||
|
self.guildid = lion.guildid
|
||||||
|
self.userid = lion.userid
|
||||||
|
|
||||||
|
# A list of InventoryItems held by this user
|
||||||
|
self.inventory = inventory
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def fetch(cls, bot: LionBot, shop_data: ShopData, guildid: int, userid: int):
|
||||||
|
lion = await bot.core.lions.fetch(guildid, userid)
|
||||||
|
inventory = await shop_data.fetch_where(guildid=guildid, userid=userid)
|
||||||
|
return cls(bot, shop_data, lion, inventory)
|
||||||
|
|
||||||
|
async def refresh(self):
|
||||||
|
"""
|
||||||
|
Refresh the data for this member.
|
||||||
|
"""
|
||||||
|
self.lion = self.bot.core.lions.fetch(self.guild.id, self.user.id)
|
||||||
|
|
||||||
|
data = self.bot.get_cog('Shopping').data
|
||||||
|
self.inventory_items = await data.InventoryItem.fetch_where(userid=self.userid, guildid=self.guildid)
|
||||||
|
|
||||||
|
|
||||||
|
class ShopItem:
|
||||||
|
"""
|
||||||
|
ABC representing a purchasable guild shop item.
|
||||||
|
"""
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
async def purchase(self, userid):
|
||||||
|
"""
|
||||||
|
Called when a member purchases this item.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Shop:
|
||||||
|
"""
|
||||||
|
Base class representing a Shop for a particular member.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot: LionBot, shop_data: ShopData, member: discord.Member):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = shop_data
|
||||||
|
self.member = member
|
||||||
|
self.guild = member.guild
|
||||||
|
|
||||||
|
# A list of ShopItems that are currently visible to the member
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
# Current inventory for the member
|
||||||
|
self.inventory = None
|
||||||
|
|
||||||
|
async def refresh(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Store(ui.LeoUI):
|
||||||
|
"""
|
||||||
|
Base UI for the different shops.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot: LionBot, data, shops):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = data
|
||||||
|
self.shops = shops
|
||||||
0
bot/modules/shop/shops/boosts.py
Normal file
0
bot/modules/shop/shops/boosts.py
Normal file
147
bot/modules/shop/shops/colours.py
Normal file
147
bot/modules/shop/shops/colours.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands as cmds
|
||||||
|
from discord import app_commands as appcmds
|
||||||
|
from discord.ui.select import select, Select
|
||||||
|
from discord.ui.button import button, Button
|
||||||
|
|
||||||
|
from meta import LionCog, LionContext, LionBot
|
||||||
|
from utils import ui
|
||||||
|
|
||||||
|
from .data import ShopData
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..cog import Shopping
|
||||||
|
|
||||||
|
|
||||||
|
class ColourShopping(LionCog):
|
||||||
|
"""
|
||||||
|
Cog in charge of colour shopping.
|
||||||
|
|
||||||
|
Registers colour shop related commands and methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bot: LionBot, data):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
async def load_into(self, cog: 'Shopping'):
|
||||||
|
self.crossload_group(self.editshop_group, cog.editshop_group)
|
||||||
|
await cog.bot.add_cog(self)
|
||||||
|
|
||||||
|
async def unload(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@LionCog.placeholder_group
|
||||||
|
@cmds.hybrid_group('editshopp', with_app_command=False)
|
||||||
|
async def editshop_group(self, ctx: LionContext):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@editshop_group.group('colours')
|
||||||
|
async def editshop_colour_group(self, ctx: LionContext):
|
||||||
|
...
|
||||||
|
|
||||||
|
@editshop_colour_group.command('edit')
|
||||||
|
async def editshop_colours_edit_cmd(self, ctx: LionContext):
|
||||||
|
await ctx.reply(f"I am a {self.__class__.__name__} version 2")
|
||||||
|
...
|
||||||
|
|
||||||
|
def make_widget(self):
|
||||||
|
"""
|
||||||
|
Instantiate and return a new UI for this shop.
|
||||||
|
"""
|
||||||
|
return ColourStore(self.bot, self.data)
|
||||||
|
|
||||||
|
def make_shop_for(self, member: discord.Member):
|
||||||
|
return ColourShop(member, self.data)
|
||||||
|
|
||||||
|
|
||||||
|
class ColourShop:
|
||||||
|
"""
|
||||||
|
A Shop representing a colour shop for a particular member.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
bot: LionBot
|
||||||
|
The current LionBot.
|
||||||
|
|
||||||
|
member: discord.Member
|
||||||
|
The member this particular shop is for.
|
||||||
|
|
||||||
|
data: ShopData
|
||||||
|
An initialised ShopData registry.
|
||||||
|
"""
|
||||||
|
def __init__(self, bot, member, data):
|
||||||
|
self.bot = bot
|
||||||
|
self.user = member
|
||||||
|
self.guild = member.guild
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
# List of items in this shop. Initialised in refresh()
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
# Current inventory for this member
|
||||||
|
self.inventory = None
|
||||||
|
|
||||||
|
def make_store(self):
|
||||||
|
"""
|
||||||
|
Initialise and return a new Store UI for this shop.
|
||||||
|
"""
|
||||||
|
return ColourStore(self)
|
||||||
|
|
||||||
|
|
||||||
|
class ColourStore:
|
||||||
|
"""
|
||||||
|
Ephemeral UI providing access to the colour store.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, shop: ColourShop):
|
||||||
|
self.bot = shop.bot
|
||||||
|
self.data = shop.data
|
||||||
|
self.shop = shop
|
||||||
|
|
||||||
|
self.shop_row = ()
|
||||||
|
|
||||||
|
async def refresh(self):
|
||||||
|
"""
|
||||||
|
Refresh the data.
|
||||||
|
"""
|
||||||
|
# Refresh current item list
|
||||||
|
# Refresh user's current item
|
||||||
|
...
|
||||||
|
|
||||||
|
async def redraw(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
@select(placeholder="Select to Buy")
|
||||||
|
async def select_colour(self, interaction: discord.Interaction, selection: Select):
|
||||||
|
# User selected a colour from the list
|
||||||
|
# Run purchase pathway for that item
|
||||||
|
...
|
||||||
|
|
||||||
|
async def select_colour_refresh(self):
|
||||||
|
"""
|
||||||
|
Refresh the select colour menu.
|
||||||
|
|
||||||
|
For an item to be purchasable,
|
||||||
|
it needs to be affordable and not currently owned by the member.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def make_embed(self):
|
||||||
|
"""
|
||||||
|
Embed for this shop.
|
||||||
|
"""
|
||||||
|
lines = []
|
||||||
|
for i, item in enumerate(self.shop.items):
|
||||||
|
line = f"[{i+1}] | `{item.price} LC` | <@&{item.data.roleid}>"
|
||||||
|
if item.itemid in self.shop.member_inventory.itemids:
|
||||||
|
line += " (You own this!)"
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Colour Role Shop",
|
||||||
|
description=""
|
||||||
|
)
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
0
bot/modules/shop/shops/roles.py
Normal file
0
bot/modules/shop/shops/roles.py
Normal file
@@ -1,3 +1,5 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
-- Add metdata to configuration tables {{{
|
-- Add metdata to configuration tables {{{
|
||||||
ALTER TABLE user_config ADD COLUMN name TEXT;
|
ALTER TABLE user_config ADD COLUMN name TEXT;
|
||||||
ALTER TABLE user_config ADD COLUMN first_seen TIMESTAMPTZ DEFAULT now();
|
ALTER TABLE user_config ADD COLUMN first_seen TIMESTAMPTZ DEFAULT now();
|
||||||
@@ -205,9 +207,25 @@ CREATE TABLE coin_transactions_admin_actions(
|
|||||||
PRIMARY KEY (actionid, transactionid)
|
PRIMARY KEY (actionid, transactionid)
|
||||||
);
|
);
|
||||||
CREATE INDEX coin_transactions_admin_actions_transactionid ON coin_transactions_admin_actions (transactionid);
|
CREATE INDEX coin_transactions_admin_actions_transactionid ON coin_transactions_admin_actions (transactionid);
|
||||||
|
-- }}}
|
||||||
|
|
||||||
|
-- Shop data {{{
|
||||||
|
ALTER TABLE member_inventory DROP CONSTRAINT member_inventory_pkey;
|
||||||
|
|
||||||
|
ALTER TABLE member_inventory
|
||||||
|
ADD COLUMN inventoryid SERIAL PRIMARY KEY;
|
||||||
|
|
||||||
|
ALTER TABLE member_inventory
|
||||||
|
ADD COLUMN transactionid INTEGER REFERENCES coin_transactions (transactionid) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE member_inventory
|
||||||
|
DROP COLUMN count;
|
||||||
|
|
||||||
|
CREATE INDEX member_inventory_members ON member_inventory(guildid, userid);
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
INSERT INTO VersionHistory (version, author) VALUES (13, 'v12-v13 migration');
|
INSERT INTO VersionHistory (version, author) VALUES (13, 'v12-v13 migration');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
-- vim: set fdm=marker:
|
-- vim: set fdm=marker:
|
||||||
|
|||||||
Reference in New Issue
Block a user