diff --git a/src/modules/economy/__init__.py b/src/modules/economy/__init__.py index 25e4e6b3..3e20cce0 100644 --- a/src/modules/economy/__init__.py +++ b/src/modules/economy/__init__.py @@ -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)) diff --git a/src/modules/economy/cog.py b/src/modules/economy/cog.py index 73d6296b..127e294c 100644 --- a/src/modules/economy/cog.py +++ b/src/modules/economy/cog.py @@ -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() diff --git a/src/modules/economy/data.py b/src/modules/economy/data.py new file mode 100644 index 00000000..ac9b39f1 --- /dev/null +++ b/src/modules/economy/data.py @@ -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) diff --git a/src/modules/economy/settings.py b/src/modules/economy/settings.py new file mode 100644 index 00000000..8403b9ba --- /dev/null +++ b/src/modules/economy/settings.py @@ -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 diff --git a/src/modules/economy/settingui.py b/src/modules/economy/settingui.py new file mode 100644 index 00000000..8876d5a1 --- /dev/null +++ b/src/modules/economy/settingui.py @@ -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