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):
|
||||
from .cog import Economy
|
||||
|
||||
await bot.add_cog(Economy(bot))
|
||||
|
||||
@@ -1,265 +1,29 @@
|
||||
from typing import Optional, Union
|
||||
from enum import Enum
|
||||
|
||||
import discord
|
||||
from discord.ext import commands as cmds
|
||||
from discord import app_commands as appcmds
|
||||
|
||||
from psycopg import sql
|
||||
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 import LionCog, LionBot, LionContext, conf
|
||||
from meta.errors import ResponseTimedOut
|
||||
from babel import LocalBabel
|
||||
from data import ORDER
|
||||
|
||||
from core.data import CoreData
|
||||
|
||||
from utils.ui import LeoUI, LeoModal, Confirm, Pager
|
||||
from utils.ui import Confirm, Pager
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Commands
|
||||
@@ -284,13 +48,25 @@ class Economy(LionCog):
|
||||
def __init__(self, bot: LionBot):
|
||||
self.bot = bot
|
||||
self.data = bot.db.load_registry(EconomyData())
|
||||
self.settings = EconomySettings()
|
||||
|
||||
self.bonuses = {}
|
||||
|
||||
async def cog_load(self):
|
||||
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):
|
||||
name = name or bonus_coro.__name__
|
||||
self.bonuses[name] = bonus_coro
|
||||
@@ -594,10 +370,11 @@ class Economy(LionCog):
|
||||
)
|
||||
)
|
||||
else:
|
||||
await ctx.interaction.response.defer()
|
||||
# Viewing route
|
||||
MemModel = self.bot.core.data.Member
|
||||
if role:
|
||||
query = MemModel.fetch_where(
|
||||
query = MemModel.table.select_where(
|
||||
(MemModel.guildid == role.guild.id) & (MemModel.coins != 0)
|
||||
)
|
||||
query.order_by('coins', ORDER.DESC)
|
||||
@@ -618,7 +395,7 @@ class Economy(LionCog):
|
||||
"This server has a total balance of {coin_emoji}**{total}**."
|
||||
)).format(
|
||||
coin_emoji=cemoji,
|
||||
total=sum(row.coins for row in rows)
|
||||
total=sum(row['coins'] for row in rows)
|
||||
)
|
||||
else:
|
||||
header = t(_p(
|
||||
@@ -628,7 +405,7 @@ class Economy(LionCog):
|
||||
)).format(
|
||||
count=len(targets),
|
||||
role_mention=role.mention,
|
||||
total=sum(row.coins for row in rows),
|
||||
total=sum(row['coins'] for row in rows),
|
||||
coin_emoji=cemoji
|
||||
)
|
||||
|
||||
@@ -645,13 +422,13 @@ class Economy(LionCog):
|
||||
for i, block in enumerate(blocks):
|
||||
lines = []
|
||||
numwidth = len(str(i + len(block)))
|
||||
coinwidth = len(str(max(row.coins for row in rows)))
|
||||
for j, row in enumerate(block, start=i):
|
||||
coinwidth = len(str(max(row['coins'] for row in rows)))
|
||||
for j, row in enumerate(block, start=i*blocklen):
|
||||
lines.append(
|
||||
lb_format.format(
|
||||
pos=j, numwidth=numwidth,
|
||||
coins=row.coins, coinwidth=coinwidth,
|
||||
mention=f"<@{row.userid}>"
|
||||
coins=row['coins'], coinwidth=coinwidth,
|
||||
mention=f"<@{row['userid']}>"
|
||||
)
|
||||
)
|
||||
lb_block = '\n'.join(lines)
|
||||
@@ -906,6 +683,18 @@ class Economy(LionCog):
|
||||
return
|
||||
|
||||
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
|
||||
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?"
|
||||
))
|
||||
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