rewrite: Add economy config.
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
from .cog import Economy
|
import logging
|
||||||
|
from babel.translator import LocalBabel
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
babel = LocalBabel('economy')
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
async def setup(bot):
|
||||||
|
from .cog import Economy
|
||||||
|
|
||||||
await bot.add_cog(Economy(bot))
|
await bot.add_cog(Economy(bot))
|
||||||
|
|||||||
@@ -1,265 +1,29 @@
|
|||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands as cmds
|
from discord.ext import commands as cmds
|
||||||
from discord import app_commands as appcmds
|
from discord import app_commands as appcmds
|
||||||
|
|
||||||
from psycopg import sql
|
from meta import LionCog, LionBot, LionContext, conf
|
||||||
from data import Registry, RowModel, RegisterEnum, ORDER, JOINTYPE, RawExpr
|
|
||||||
from data.columns import Integer, Bool, String, Column, Timestamp
|
|
||||||
|
|
||||||
from meta import LionCog, LionBot, LionContext
|
|
||||||
from meta.errors import ResponseTimedOut
|
from meta.errors import ResponseTimedOut
|
||||||
from babel import LocalBabel
|
from babel import LocalBabel
|
||||||
|
from data import ORDER
|
||||||
|
|
||||||
from core.data import CoreData
|
from utils.ui import Confirm, Pager
|
||||||
|
|
||||||
from utils.ui import LeoUI, LeoModal, Confirm, Pager
|
|
||||||
from utils.lib import error_embed, MessageArgs, utc_now
|
from utils.lib import error_embed, MessageArgs, utc_now
|
||||||
|
from wards import low_management
|
||||||
|
|
||||||
|
from . import babel, logger
|
||||||
|
from .data import EconomyData, TransactionType, AdminActionType
|
||||||
|
from .settings import EconomySettings
|
||||||
|
from .settingui import EconomyConfigUI
|
||||||
|
|
||||||
babel = LocalBabel('economy')
|
|
||||||
_, _p, _np = babel._, babel._p, babel._np
|
_, _p, _np = babel._, babel._p, babel._np
|
||||||
|
|
||||||
|
|
||||||
MAX_COINS = 2**16
|
MAX_COINS = 2**16
|
||||||
|
|
||||||
|
|
||||||
class TransactionType(Enum):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TYPE CoinTransactionType AS ENUM(
|
|
||||||
'REFUND',
|
|
||||||
'TRANSFER',
|
|
||||||
'SHOP_PURCHASE',
|
|
||||||
'STUDY_SESSION',
|
|
||||||
'ADMIN',
|
|
||||||
'TASKS'
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
REFUND = 'REFUND',
|
|
||||||
TRANSFER = 'TRANSFER',
|
|
||||||
PURCHASE = 'SHOP_PURCHASE',
|
|
||||||
SESSION = 'STUDY_SESSION',
|
|
||||||
ADMIN = 'ADMIN',
|
|
||||||
TASKS = 'TASKS',
|
|
||||||
|
|
||||||
|
|
||||||
class AdminActionTarget(Enum):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TYPE EconAdminTarget AS ENUM(
|
|
||||||
'ROLE',
|
|
||||||
'USER',
|
|
||||||
'GUILD'
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
ROLE = 'ROLE',
|
|
||||||
USER = 'USER',
|
|
||||||
GUILD = 'GUILD',
|
|
||||||
|
|
||||||
|
|
||||||
class AdminActionType(Enum):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TYPE EconAdminAction AS ENUM(
|
|
||||||
'SET',
|
|
||||||
'ADD'
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
SET = 'SET',
|
|
||||||
ADD = 'ADD',
|
|
||||||
|
|
||||||
|
|
||||||
class EconomyData(Registry, name='economy'):
|
|
||||||
_TransactionType = RegisterEnum(TransactionType, 'CoinTransactionType')
|
|
||||||
_AdminActionTarget = RegisterEnum(AdminActionTarget, 'EconAdminTarget')
|
|
||||||
_AdminActionType = RegisterEnum(AdminActionType, 'EconAdminAction')
|
|
||||||
|
|
||||||
class Transaction(RowModel):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TABLE coin_transactions(
|
|
||||||
transactionid SERIAL PRIMARY KEY,
|
|
||||||
transactiontype CoinTransactionType NOT NULL,
|
|
||||||
guildid BIGINT NOT NULL REFERENCES guild_config (guildid) ON DELETE CASCADE,
|
|
||||||
actorid BIGINT NOT NULL,
|
|
||||||
amount INTEGER NOT NULL,
|
|
||||||
bonus INTEGER NOT NULL,
|
|
||||||
from_account BIGINT,
|
|
||||||
to_account BIGINT,
|
|
||||||
refunds INTEGER REFERENCES coin_transactions (transactionid) ON DELETE SET NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT (now() at time zone 'utc')
|
|
||||||
);
|
|
||||||
CREATE INDEX coin_transaction_guilds ON coin_transactions (guildid);
|
|
||||||
"""
|
|
||||||
_tablename_ = 'coin_transactions'
|
|
||||||
|
|
||||||
transactionid = Integer(primary=True)
|
|
||||||
transactiontype: Column[TransactionType] = Column()
|
|
||||||
guildid = Integer()
|
|
||||||
actorid = Integer()
|
|
||||||
amount = Integer()
|
|
||||||
bonus = Integer()
|
|
||||||
from_account = Integer()
|
|
||||||
to_account = Integer()
|
|
||||||
refunds = Integer()
|
|
||||||
created_at = Timestamp()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def execute_transaction(
|
|
||||||
cls,
|
|
||||||
transaction_type: TransactionType,
|
|
||||||
guildid: int, actorid: int,
|
|
||||||
from_account: int, to_account: int, amount: int, bonus: int = 0,
|
|
||||||
refunds: int = None
|
|
||||||
):
|
|
||||||
transaction = await cls.create(
|
|
||||||
transactiontype=transaction_type,
|
|
||||||
guildid=guildid, actorid=actorid, amount=amount, bonus=bonus,
|
|
||||||
from_account=from_account, to_account=to_account,
|
|
||||||
refunds=refunds
|
|
||||||
)
|
|
||||||
if from_account is not None:
|
|
||||||
await CoreData.Member.table.update_where(
|
|
||||||
guildid=guildid, userid=from_account
|
|
||||||
).set(coins=(CoreData.Member.coins - (amount + bonus)))
|
|
||||||
if to_account is not None:
|
|
||||||
await CoreData.Member.table.update_where(
|
|
||||||
guildid=guildid, userid=to_account
|
|
||||||
).set(coins=(CoreData.Member.coins + (amount + bonus)))
|
|
||||||
return transaction
|
|
||||||
|
|
||||||
class ShopTransaction(RowModel):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TABLE coin_transactions_shop(
|
|
||||||
transactionid INTEGER PRIMARY KEY REFERENCES coin_transactions (transactionid) ON DELETE CASCADE,
|
|
||||||
itemid INTEGER NOT NULL REFERENCES shop_items (itemid) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
_tablename_ = 'coin_transactions_shop'
|
|
||||||
|
|
||||||
transactionid = Integer(primary=True)
|
|
||||||
itemid = Integer()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def purchase_transaction(
|
|
||||||
cls,
|
|
||||||
guildid: int, actorid: int,
|
|
||||||
userid: int, itemid: int, amount: int
|
|
||||||
):
|
|
||||||
conn = await cls._connector.get_connection()
|
|
||||||
async with conn.transaction():
|
|
||||||
row = await EconomyData.Transaction.execute_transaction(
|
|
||||||
TransactionType.PURCHASE,
|
|
||||||
guildid=guildid, actorid=actorid, from_account=userid, to_account=None,
|
|
||||||
amount=amount
|
|
||||||
)
|
|
||||||
return await cls.create(transactionid=row.transactionid, itemid=itemid)
|
|
||||||
|
|
||||||
class TaskTransaction(RowModel):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TABLE coin_transactions_tasks(
|
|
||||||
transactionid INTEGER PRIMARY KEY REFERENCES coin_transactions (transactionid) ON DELETE CASCADE,
|
|
||||||
count INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
_tablename_ = 'coin_transactions_tasks'
|
|
||||||
|
|
||||||
transactionid = Integer(primary=True)
|
|
||||||
count = Integer()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def count_recent_for(cls, userid, guildid, interval='24h'):
|
|
||||||
"""
|
|
||||||
Retrieve the number of tasks rewarded in the last `interval`.
|
|
||||||
"""
|
|
||||||
T = EconomyData.Transaction
|
|
||||||
query = cls.table.select_where().with_no_adapter()
|
|
||||||
query.join(T, using=(T.transactionid.name, ), join_type=JOINTYPE.LEFT)
|
|
||||||
query.select(recent=sql.SQL("SUM({})").format(cls.count.expr))
|
|
||||||
query.where(
|
|
||||||
T.to_account == userid,
|
|
||||||
T.guildid == guildid,
|
|
||||||
T.created_at > RawExpr(sql.SQL("timezone('utc', NOW()) - INTERVAL {}").format(interval), ()),
|
|
||||||
)
|
|
||||||
result = await query
|
|
||||||
return result[0]['recent'] or 0
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def reward_completed(cls, userid, guildid, count, amount):
|
|
||||||
"""
|
|
||||||
Reward the specified member `amount` coins for completing `count` tasks.
|
|
||||||
"""
|
|
||||||
# TODO: Bonus logic, perhaps apply_bonus(amount), or put this method in the economy cog?
|
|
||||||
conn = await cls._connector.get_connection()
|
|
||||||
async with conn.transaction():
|
|
||||||
row = await EconomyData.Transaction.execute_transaction(
|
|
||||||
TransactionType.TASKS,
|
|
||||||
guildid=guildid, actorid=userid, from_account=None, to_account=userid,
|
|
||||||
amount=amount
|
|
||||||
)
|
|
||||||
return await cls.create(transactionid=row.transactionid, count=count)
|
|
||||||
|
|
||||||
class SessionTransaction(RowModel):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TABLE coin_transactions_sessions(
|
|
||||||
transactionid INTEGER PRIMARY KEY REFERENCES coin_transactions (transactionid) ON DELETE CASCADE,
|
|
||||||
sessionid INTEGER NOT NULL REFERENCES session_history (sessionid) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
_tablename_ = 'coin_transactions_sessions'
|
|
||||||
|
|
||||||
transactionid = Integer(primary=True)
|
|
||||||
sessionid = Integer()
|
|
||||||
|
|
||||||
class AdminActions(RowModel):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TABLE economy_admin_actions(
|
|
||||||
actionid SERIAL PRIMARY KEY,
|
|
||||||
target_type EconAdminTarget NOT NULL,
|
|
||||||
action_type EconAdminAction NOT NULL,
|
|
||||||
targetid INTEGER NOT NULL,
|
|
||||||
amount INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
_tablename_ = 'economy_admin_actions'
|
|
||||||
|
|
||||||
actionid = Integer(primary=True)
|
|
||||||
target_type: Column[AdminActionTarget] = Column()
|
|
||||||
action_type: Column[AdminActionType] = Column()
|
|
||||||
targetid = Integer()
|
|
||||||
amount = Integer()
|
|
||||||
|
|
||||||
class AdminTransactions(RowModel):
|
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
------
|
|
||||||
CREATE TABLE coin_transactions_admin_actions(
|
|
||||||
actionid INTEGER NOT NULL REFERENCES economy_admin_actions (actionid),
|
|
||||||
transactionid INTEGER NOT NULL REFERENCES coin_transactions (transactionid),
|
|
||||||
PRIMARY KEY (actionid, transactionid)
|
|
||||||
);
|
|
||||||
CREATE INDEX coin_transactions_admin_actions_transactionid ON coin_transactions_admin_actions (transactionid);
|
|
||||||
"""
|
|
||||||
_tablename_ = 'coin_transactions_admin_actions'
|
|
||||||
|
|
||||||
actionid = Integer(primary=True)
|
|
||||||
transactionid = Integer(primary=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Economy(LionCog):
|
class Economy(LionCog):
|
||||||
"""
|
"""
|
||||||
Commands
|
Commands
|
||||||
@@ -284,13 +48,25 @@ class Economy(LionCog):
|
|||||||
def __init__(self, bot: LionBot):
|
def __init__(self, bot: LionBot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.data = bot.db.load_registry(EconomyData())
|
self.data = bot.db.load_registry(EconomyData())
|
||||||
|
self.settings = EconomySettings()
|
||||||
|
|
||||||
self.bonuses = {}
|
self.bonuses = {}
|
||||||
|
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
|
|
||||||
# ----- Economy Bonus regsitration -----
|
self.bot.core.guild_config.register_model_setting(self.settings.AllowTransfers)
|
||||||
|
self.bot.core.guild_config.register_model_setting(self.settings.CoinsPerXP)
|
||||||
|
|
||||||
|
configcog = self.bot.get_cog('ConfigCog')
|
||||||
|
if configcog is None:
|
||||||
|
logger.critical(
|
||||||
|
"Attempting to load the EconomyCog before ConfigCog! Failed to crossload configuration group."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.crossload_group(self.configure_group, configcog.configure_group)
|
||||||
|
|
||||||
|
# ----- Economy Bonus registration -----
|
||||||
def register_economy_bonus(self, bonus_coro, name=None):
|
def register_economy_bonus(self, bonus_coro, name=None):
|
||||||
name = name or bonus_coro.__name__
|
name = name or bonus_coro.__name__
|
||||||
self.bonuses[name] = bonus_coro
|
self.bonuses[name] = bonus_coro
|
||||||
@@ -594,10 +370,11 @@ class Economy(LionCog):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
await ctx.interaction.response.defer()
|
||||||
# Viewing route
|
# Viewing route
|
||||||
MemModel = self.bot.core.data.Member
|
MemModel = self.bot.core.data.Member
|
||||||
if role:
|
if role:
|
||||||
query = MemModel.fetch_where(
|
query = MemModel.table.select_where(
|
||||||
(MemModel.guildid == role.guild.id) & (MemModel.coins != 0)
|
(MemModel.guildid == role.guild.id) & (MemModel.coins != 0)
|
||||||
)
|
)
|
||||||
query.order_by('coins', ORDER.DESC)
|
query.order_by('coins', ORDER.DESC)
|
||||||
@@ -618,7 +395,7 @@ class Economy(LionCog):
|
|||||||
"This server has a total balance of {coin_emoji}**{total}**."
|
"This server has a total balance of {coin_emoji}**{total}**."
|
||||||
)).format(
|
)).format(
|
||||||
coin_emoji=cemoji,
|
coin_emoji=cemoji,
|
||||||
total=sum(row.coins for row in rows)
|
total=sum(row['coins'] for row in rows)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
header = t(_p(
|
header = t(_p(
|
||||||
@@ -628,7 +405,7 @@ class Economy(LionCog):
|
|||||||
)).format(
|
)).format(
|
||||||
count=len(targets),
|
count=len(targets),
|
||||||
role_mention=role.mention,
|
role_mention=role.mention,
|
||||||
total=sum(row.coins for row in rows),
|
total=sum(row['coins'] for row in rows),
|
||||||
coin_emoji=cemoji
|
coin_emoji=cemoji
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -645,13 +422,13 @@ class Economy(LionCog):
|
|||||||
for i, block in enumerate(blocks):
|
for i, block in enumerate(blocks):
|
||||||
lines = []
|
lines = []
|
||||||
numwidth = len(str(i + len(block)))
|
numwidth = len(str(i + len(block)))
|
||||||
coinwidth = len(str(max(row.coins for row in rows)))
|
coinwidth = len(str(max(row['coins'] for row in rows)))
|
||||||
for j, row in enumerate(block, start=i):
|
for j, row in enumerate(block, start=i*blocklen):
|
||||||
lines.append(
|
lines.append(
|
||||||
lb_format.format(
|
lb_format.format(
|
||||||
pos=j, numwidth=numwidth,
|
pos=j, numwidth=numwidth,
|
||||||
coins=row.coins, coinwidth=coinwidth,
|
coins=row['coins'], coinwidth=coinwidth,
|
||||||
mention=f"<@{row.userid}>"
|
mention=f"<@{row['userid']}>"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
lb_block = '\n'.join(lines)
|
lb_block = '\n'.join(lines)
|
||||||
@@ -906,6 +683,18 @@ class Economy(LionCog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
if not ctx.lguild.config.get('allow_transfers').value:
|
||||||
|
await ctx.interaction.response.send_message(
|
||||||
|
embed=error_embed(
|
||||||
|
t(_p(
|
||||||
|
'cmd:send|error:not_allowed',
|
||||||
|
"Sorry, this server has disabled LionCoin transfers!"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
Member = self.bot.core.data.Member
|
Member = self.bot.core.data.Member
|
||||||
target_lion = await self.bot.core.lions.fetch_member(ctx.guild.id, target.id)
|
target_lion = await self.bot.core.lions.fetch_member(ctx.guild.id, target.id)
|
||||||
|
|
||||||
@@ -985,3 +774,54 @@ class Economy(LionCog):
|
|||||||
"Unfortunately, I was not able to message the recipient. Perhaps they have me blocked?"
|
"Unfortunately, I was not able to message the recipient. Perhaps they have me blocked?"
|
||||||
))
|
))
|
||||||
await ctx.interaction.edit_original_response(embed=embed)
|
await ctx.interaction.edit_original_response(embed=embed)
|
||||||
|
|
||||||
|
# -------- Configuration Commands --------
|
||||||
|
@LionCog.placeholder_group
|
||||||
|
@cmds.hybrid_group('configure', with_app_command=False)
|
||||||
|
async def configure_group(self, ctx: LionContext):
|
||||||
|
# Placeholder group method, not used
|
||||||
|
pass
|
||||||
|
|
||||||
|
@configure_group.command(
|
||||||
|
name=_p('cmd:configure_economy', "economy"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:configure_economy|desc',
|
||||||
|
"Configure LionCoin Economy"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@cmds.check(low_management)
|
||||||
|
async def configure_economy(self, ctx: LionContext,
|
||||||
|
allow_transfers: Optional[bool] = None,
|
||||||
|
coins_per_xp: Optional[appcmds.Range[int, 0, 2**15]] = None):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
setting_allow_transfers = ctx.lguild.config.get('allow_transfers')
|
||||||
|
setting_coins_per_xp = ctx.lguild.config.get('coins_per_xp')
|
||||||
|
|
||||||
|
modified = []
|
||||||
|
if allow_transfers is not None:
|
||||||
|
setting_allow_transfers.data = allow_transfers
|
||||||
|
await setting_allow_transfers.write()
|
||||||
|
modified.append(setting_allow_transfers)
|
||||||
|
if coins_per_xp is not None:
|
||||||
|
setting_coins_per_xp.data = coins_per_xp
|
||||||
|
await setting_coins_per_xp.write()
|
||||||
|
modified.append(setting_coins_per_xp)
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
desc = '\n'.join(f"{conf.emojis.tick} {setting.update_message}" for setting in modified)
|
||||||
|
await ctx.reply(
|
||||||
|
embed=discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
description=desc
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx.channel.id not in EconomyConfigUI._listening or not modified:
|
||||||
|
configui = EconomyConfigUI(self.bot, ctx.guild.id, ctx.channel.id)
|
||||||
|
await configui.run(ctx.interaction)
|
||||||
|
await configui.wait()
|
||||||
|
|||||||
242
src/modules/economy/data.py
Normal file
242
src/modules/economy/data.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from psycopg import sql
|
||||||
|
from data import Registry, RowModel, RegisterEnum, JOINTYPE, RawExpr
|
||||||
|
from data.columns import Integer, Bool, Column, Timestamp
|
||||||
|
|
||||||
|
from core.data import CoreData
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionType(Enum):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TYPE CoinTransactionType AS ENUM(
|
||||||
|
'REFUND',
|
||||||
|
'TRANSFER',
|
||||||
|
'SHOP_PURCHASE',
|
||||||
|
'STUDY_SESSION',
|
||||||
|
'ADMIN',
|
||||||
|
'TASKS'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
REFUND = 'REFUND',
|
||||||
|
TRANSFER = 'TRANSFER',
|
||||||
|
PURCHASE = 'SHOP_PURCHASE',
|
||||||
|
SESSION = 'STUDY_SESSION',
|
||||||
|
ADMIN = 'ADMIN',
|
||||||
|
TASKS = 'TASKS',
|
||||||
|
|
||||||
|
|
||||||
|
class AdminActionTarget(Enum):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TYPE EconAdminTarget AS ENUM(
|
||||||
|
'ROLE',
|
||||||
|
'USER',
|
||||||
|
'GUILD'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
ROLE = 'ROLE',
|
||||||
|
USER = 'USER',
|
||||||
|
GUILD = 'GUILD',
|
||||||
|
|
||||||
|
|
||||||
|
class AdminActionType(Enum):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TYPE EconAdminAction AS ENUM(
|
||||||
|
'SET',
|
||||||
|
'ADD'
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
SET = 'SET',
|
||||||
|
ADD = 'ADD',
|
||||||
|
|
||||||
|
|
||||||
|
class EconomyData(Registry, name='economy'):
|
||||||
|
_TransactionType = RegisterEnum(TransactionType, 'CoinTransactionType')
|
||||||
|
_AdminActionTarget = RegisterEnum(AdminActionTarget, 'EconAdminTarget')
|
||||||
|
_AdminActionType = RegisterEnum(AdminActionType, 'EconAdminAction')
|
||||||
|
|
||||||
|
class Transaction(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE coin_transactions(
|
||||||
|
transactionid SERIAL PRIMARY KEY,
|
||||||
|
transactiontype CoinTransactionType NOT NULL,
|
||||||
|
guildid BIGINT NOT NULL REFERENCES guild_config (guildid) ON DELETE CASCADE,
|
||||||
|
actorid BIGINT NOT NULL,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
bonus INTEGER NOT NULL,
|
||||||
|
from_account BIGINT,
|
||||||
|
to_account BIGINT,
|
||||||
|
refunds INTEGER REFERENCES coin_transactions (transactionid) ON DELETE SET NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT (now() at time zone 'utc')
|
||||||
|
);
|
||||||
|
CREATE INDEX coin_transaction_guilds ON coin_transactions (guildid);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'coin_transactions'
|
||||||
|
|
||||||
|
transactionid = Integer(primary=True)
|
||||||
|
transactiontype: Column[TransactionType] = Column()
|
||||||
|
guildid = Integer()
|
||||||
|
actorid = Integer()
|
||||||
|
amount = Integer()
|
||||||
|
bonus = Integer()
|
||||||
|
from_account = Integer()
|
||||||
|
to_account = Integer()
|
||||||
|
refunds = Integer()
|
||||||
|
created_at = Timestamp()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute_transaction(
|
||||||
|
cls,
|
||||||
|
transaction_type: TransactionType,
|
||||||
|
guildid: int, actorid: int,
|
||||||
|
from_account: int, to_account: int, amount: int, bonus: int = 0,
|
||||||
|
refunds: int = None
|
||||||
|
):
|
||||||
|
transaction = await cls.create(
|
||||||
|
transactiontype=transaction_type,
|
||||||
|
guildid=guildid, actorid=actorid, amount=amount, bonus=bonus,
|
||||||
|
from_account=from_account, to_account=to_account,
|
||||||
|
refunds=refunds
|
||||||
|
)
|
||||||
|
if from_account is not None:
|
||||||
|
await CoreData.Member.table.update_where(
|
||||||
|
guildid=guildid, userid=from_account
|
||||||
|
).set(coins=(CoreData.Member.coins - (amount + bonus)))
|
||||||
|
if to_account is not None:
|
||||||
|
await CoreData.Member.table.update_where(
|
||||||
|
guildid=guildid, userid=to_account
|
||||||
|
).set(coins=(CoreData.Member.coins + (amount + bonus)))
|
||||||
|
return transaction
|
||||||
|
|
||||||
|
class ShopTransaction(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE coin_transactions_shop(
|
||||||
|
transactionid INTEGER PRIMARY KEY REFERENCES coin_transactions (transactionid) ON DELETE CASCADE,
|
||||||
|
itemid INTEGER NOT NULL REFERENCES shop_items (itemid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'coin_transactions_shop'
|
||||||
|
|
||||||
|
transactionid = Integer(primary=True)
|
||||||
|
itemid = Integer()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def purchase_transaction(
|
||||||
|
cls,
|
||||||
|
guildid: int, actorid: int,
|
||||||
|
userid: int, itemid: int, amount: int
|
||||||
|
):
|
||||||
|
conn = await cls._connector.get_connection()
|
||||||
|
async with conn.transaction():
|
||||||
|
row = await EconomyData.Transaction.execute_transaction(
|
||||||
|
TransactionType.PURCHASE,
|
||||||
|
guildid=guildid, actorid=actorid, from_account=userid, to_account=None,
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
return await cls.create(transactionid=row.transactionid, itemid=itemid)
|
||||||
|
|
||||||
|
class TaskTransaction(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE coin_transactions_tasks(
|
||||||
|
transactionid INTEGER PRIMARY KEY REFERENCES coin_transactions (transactionid) ON DELETE CASCADE,
|
||||||
|
count INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'coin_transactions_tasks'
|
||||||
|
|
||||||
|
transactionid = Integer(primary=True)
|
||||||
|
count = Integer()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def count_recent_for(cls, userid, guildid, interval='24h'):
|
||||||
|
"""
|
||||||
|
Retrieve the number of tasks rewarded in the last `interval`.
|
||||||
|
"""
|
||||||
|
T = EconomyData.Transaction
|
||||||
|
query = cls.table.select_where().with_no_adapter()
|
||||||
|
query.join(T, using=(T.transactionid.name, ), join_type=JOINTYPE.LEFT)
|
||||||
|
query.select(recent=sql.SQL("SUM({})").format(cls.count.expr))
|
||||||
|
query.where(
|
||||||
|
T.to_account == userid,
|
||||||
|
T.guildid == guildid,
|
||||||
|
T.created_at > RawExpr(sql.SQL("timezone('utc', NOW()) - INTERVAL {}").format(interval), ()),
|
||||||
|
)
|
||||||
|
result = await query
|
||||||
|
return result[0]['recent'] or 0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def reward_completed(cls, userid, guildid, count, amount):
|
||||||
|
"""
|
||||||
|
Reward the specified member `amount` coins for completing `count` tasks.
|
||||||
|
"""
|
||||||
|
# TODO: Bonus logic, perhaps apply_bonus(amount), or put this method in the economy cog?
|
||||||
|
conn = await cls._connector.get_connection()
|
||||||
|
async with conn.transaction():
|
||||||
|
row = await EconomyData.Transaction.execute_transaction(
|
||||||
|
TransactionType.TASKS,
|
||||||
|
guildid=guildid, actorid=userid, from_account=None, to_account=userid,
|
||||||
|
amount=amount
|
||||||
|
)
|
||||||
|
return await cls.create(transactionid=row.transactionid, count=count)
|
||||||
|
|
||||||
|
class SessionTransaction(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE coin_transactions_sessions(
|
||||||
|
transactionid INTEGER PRIMARY KEY REFERENCES coin_transactions (transactionid) ON DELETE CASCADE,
|
||||||
|
sessionid INTEGER NOT NULL REFERENCES session_history (sessionid) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'coin_transactions_sessions'
|
||||||
|
|
||||||
|
transactionid = Integer(primary=True)
|
||||||
|
sessionid = Integer()
|
||||||
|
|
||||||
|
class AdminActions(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE economy_admin_actions(
|
||||||
|
actionid SERIAL PRIMARY KEY,
|
||||||
|
target_type EconAdminTarget NOT NULL,
|
||||||
|
action_type EconAdminAction NOT NULL,
|
||||||
|
targetid INTEGER NOT NULL,
|
||||||
|
amount INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'economy_admin_actions'
|
||||||
|
|
||||||
|
actionid = Integer(primary=True)
|
||||||
|
target_type: Column[AdminActionTarget] = Column()
|
||||||
|
action_type: Column[AdminActionType] = Column()
|
||||||
|
targetid = Integer()
|
||||||
|
amount = Integer()
|
||||||
|
|
||||||
|
class AdminTransactions(RowModel):
|
||||||
|
"""
|
||||||
|
Schema
|
||||||
|
------
|
||||||
|
CREATE TABLE coin_transactions_admin_actions(
|
||||||
|
actionid INTEGER NOT NULL REFERENCES economy_admin_actions (actionid),
|
||||||
|
transactionid INTEGER NOT NULL REFERENCES coin_transactions (transactionid),
|
||||||
|
PRIMARY KEY (actionid, transactionid)
|
||||||
|
);
|
||||||
|
CREATE INDEX coin_transactions_admin_actions_transactionid ON coin_transactions_admin_actions (transactionid);
|
||||||
|
"""
|
||||||
|
_tablename_ = 'coin_transactions_admin_actions'
|
||||||
|
|
||||||
|
actionid = Integer(primary=True)
|
||||||
|
transactionid = Integer(primary=True)
|
||||||
72
src/modules/economy/settings.py
Normal file
72
src/modules/economy/settings.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Settings for the Economy Cog.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
import asyncio
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from settings.groups import SettingGroup
|
||||||
|
from settings.data import ModelData, ListData
|
||||||
|
from settings.setting_types import ChannelListSetting, IntegerSetting, BoolSetting
|
||||||
|
|
||||||
|
from meta.config import conf
|
||||||
|
from meta.sharding import THIS_SHARD
|
||||||
|
from meta.logger import log_wrap
|
||||||
|
from core.data import CoreData
|
||||||
|
from babel.translator import ctx_translator
|
||||||
|
|
||||||
|
from . import babel, logger
|
||||||
|
from .data import EconomyData
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class EconomySettings(SettingGroup):
|
||||||
|
"""
|
||||||
|
Economy Settings:
|
||||||
|
coins_per_100xp
|
||||||
|
allow_transfers
|
||||||
|
"""
|
||||||
|
class CoinsPerXP(ModelData, IntegerSetting):
|
||||||
|
setting_id = 'coins_per_xp'
|
||||||
|
|
||||||
|
_display_name = _p('guildset:coins_per_xp', "coins_per_100xp")
|
||||||
|
_desc = _p(
|
||||||
|
'guildset:coins_per_xp|desc',
|
||||||
|
"How many LionCoins to reward members per 100 XP they earn."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'guildset:coins_per_xp|long_desc',
|
||||||
|
"Members will be rewarded with this many LionCoins for every 100 XP they earn."
|
||||||
|
)
|
||||||
|
# This default needs to dynamically depend on the guild mode!
|
||||||
|
_default = 50
|
||||||
|
|
||||||
|
_model = CoreData.Guild
|
||||||
|
_column = CoreData.Guild.coins_per_centixp.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def update_message(self):
|
||||||
|
t = ctx_translator.get().t
|
||||||
|
return t(_p(
|
||||||
|
'guildset:coins_per_xp|set_response',
|
||||||
|
"For every **100** XP they earn, members will now be given {coin}**{amount}**."
|
||||||
|
)).format(amount=self.value, coin=conf.emojis.coin)
|
||||||
|
|
||||||
|
class AllowTransfers(ModelData, BoolSetting):
|
||||||
|
setting_id = 'allow_transfers'
|
||||||
|
|
||||||
|
_display_name = _p('guildset:allow_transfers', "allow_transfers")
|
||||||
|
_desc = _p(
|
||||||
|
'guildset:allow_transfers|desc',
|
||||||
|
"Whether to allow members to transfer LionCoins to each other."
|
||||||
|
)
|
||||||
|
_long_desc = _p(
|
||||||
|
'guildset:allow_transfers|long_desc',
|
||||||
|
"If disabled, members will not be able to use `/sendcoins` to transfer LionCoinds."
|
||||||
|
)
|
||||||
|
_default = True
|
||||||
|
|
||||||
|
_model = CoreData.Guild
|
||||||
|
_column = CoreData.Guild.allow_transfers.name
|
||||||
71
src/modules/economy/settingui.py
Normal file
71
src/modules/economy/settingui.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ui.select import select, Select, ChannelSelect
|
||||||
|
from discord.ui.button import button, Button, ButtonStyle
|
||||||
|
|
||||||
|
from meta import LionBot
|
||||||
|
|
||||||
|
from utils.ui import ConfigUI, DashboardSection
|
||||||
|
from utils.lib import MessageArgs
|
||||||
|
|
||||||
|
from .settings import EconomySettings
|
||||||
|
from . import babel
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class EconomyConfigUI(ConfigUI):
|
||||||
|
setting_classes = (
|
||||||
|
EconomySettings.CoinsPerXP,
|
||||||
|
EconomySettings.AllowTransfers
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, bot: LionBot,
|
||||||
|
guildid: int, channelid: int, **kwargs):
|
||||||
|
self.settings = bot.get_cog('Economy').settings
|
||||||
|
super().__init__(bot, guildid, channelid, **kwargs)
|
||||||
|
|
||||||
|
async def make_message(self) -> MessageArgs:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
title = t(_p(
|
||||||
|
'ui:economy_config|embed|title',
|
||||||
|
"Economy Configuration Panel"
|
||||||
|
))
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.orange(),
|
||||||
|
title=title
|
||||||
|
)
|
||||||
|
for setting in self.instances:
|
||||||
|
embed.add_field(**setting.embed_field, inline=False)
|
||||||
|
|
||||||
|
args = MessageArgs(embed=embed)
|
||||||
|
return args
|
||||||
|
|
||||||
|
async def reload(self):
|
||||||
|
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
|
||||||
|
coins_per_xp = lguild.config.get(self.settings.CoinsPerXP.setting_id)
|
||||||
|
allow_transfers = lguild.config.get(self.settings.AllowTransfers.setting_id)
|
||||||
|
self.instances = (
|
||||||
|
coins_per_xp,
|
||||||
|
allow_transfers
|
||||||
|
)
|
||||||
|
|
||||||
|
async def refresh_components(self):
|
||||||
|
await asyncio.gather(
|
||||||
|
self.edit_button_refresh(),
|
||||||
|
self.close_button_refresh(),
|
||||||
|
self.reset_button_refresh(),
|
||||||
|
)
|
||||||
|
self._layout = [
|
||||||
|
(self.edit_button, self.reset_button, self.close_button),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class EconomyDashboard(DashboardSection):
|
||||||
|
section_name = _p(
|
||||||
|
'dash:economy|title',
|
||||||
|
"Economy Configuration"
|
||||||
|
)
|
||||||
|
configui = EconomyConfigUI
|
||||||
|
setting_classes = EconomyConfigUI.setting_classes
|
||||||
Reference in New Issue
Block a user