783 lines
35 KiB
Python
783 lines
35 KiB
Python
import calendar
|
|
import io
|
|
import logging
|
|
import typing as t
|
|
from datetime import datetime, timezone
|
|
|
|
import discord
|
|
from discord import app_commands
|
|
from redbot.core import Config, bank, commands
|
|
from redbot.core.i18n import Translator, cog_i18n
|
|
from redbot.core.utils.chat_formatting import humanize_number, humanize_timedelta
|
|
|
|
from ..abc import MixinMeta
|
|
from ..common.models import CommandCost
|
|
from ..common.parser import SetParser
|
|
from ..views.confirm import ConfirmView
|
|
from ..views.cost_menu import CostMenu
|
|
|
|
log = logging.getLogger("red.vrt.extendedeconomy.admin")
|
|
_ = Translator("ExtendedEconomy", __file__)
|
|
|
|
|
|
@cog_i18n(_)
|
|
class Admin(MixinMeta):
|
|
@commands.group(aliases=["ecoset", "exteco"])
|
|
@commands.admin_or_permissions(manage_guild=True)
|
|
@commands.guild_only()
|
|
async def extendedeconomy(self, ctx: commands.Context):
|
|
"""
|
|
Extended Economy settings
|
|
|
|
**NOTE**
|
|
Although setting prices for pure slash commands works, there is no refund mechanism in place for them.
|
|
|
|
Should a hybrid or text command fail due to an unhandled exception, the user will be refunded.
|
|
"""
|
|
pass
|
|
|
|
@extendedeconomy.command(name="diagnose", hidden=True)
|
|
@commands.guildowner()
|
|
async def diagnose_issues(self, ctx: commands.Context, *, member: discord.Member):
|
|
"""
|
|
Diagnose issues with the cog for a user
|
|
"""
|
|
eco = self.bot.get_cog("Economy")
|
|
is_global = await bank.is_global()
|
|
|
|
txt = _("**Global Bank:** `{}`\n").format(is_global)
|
|
txt += _("**Economy Cog:** `{}`\n").format(_("Loaded") if eco else _("Not Loaded"))
|
|
txt += _("**Auto Paydays:** `{}`\n").format(self.db.auto_payday_claim)
|
|
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if not is_global:
|
|
txt += _("**Auto Payday Roles:** {}\n").format(
|
|
", ".join([f"<@&{x}>" for x in conf.auto_claim_roles]) if conf.auto_claim_roles else _("None")
|
|
)
|
|
|
|
cur_time = calendar.timegm(datetime.now(tz=timezone.utc).utctimetuple())
|
|
|
|
eco_conf: Config = eco.config
|
|
if is_global:
|
|
bankgroup = bank._config._get_base_group(bank._config.USER)
|
|
ecogroup = eco_conf._get_base_group(eco_conf.USER)
|
|
accounts: t.Dict[str, dict] = await bankgroup.all()
|
|
ecousers: t.Dict[str, dict] = await ecogroup.all()
|
|
# max_bal = await bank.get_max_balance()
|
|
payday_time = await eco_conf.PAYDAY_TIME()
|
|
# payday_credits = await eco_conf.PAYDAY_CREDITS()
|
|
else:
|
|
bankgroup = bank._config._get_base_group(bank._config.MEMBER, str(ctx.guild.id))
|
|
ecogroup = eco_conf._get_base_group(eco_conf.MEMBER, str(ctx.guild.id))
|
|
accounts: t.Dict[str, dict] = await bankgroup.all()
|
|
ecousers: t.Dict[str, dict] = await ecogroup.all()
|
|
# max_bal = await bank.get_max_balance(ctx.guild)
|
|
payday_time = await eco_conf.guild(ctx.guild).PAYDAY_TIME()
|
|
# payday_credits = await eco_conf.guild(ctx.guild).PAYDAY_CREDITS()
|
|
# payday_roles: t.Dict[int, dict] = await eco_conf.all_roles()
|
|
|
|
uid = str(member.id)
|
|
if uid not in accounts:
|
|
txt += _("- {} has not used the bank yet.\n").format(member.display_name)
|
|
|
|
if uid not in ecousers:
|
|
txt += _("- {} has not used the economy commands yet.\n").format(member.display_name)
|
|
else:
|
|
next_payday = ecousers[uid].get("next_payday", 0) + payday_time
|
|
if cur_time < next_payday:
|
|
time_left = next_payday - cur_time
|
|
txt += _("- {} has {} seconds left until their next payday.\n").format(member.display_name, time_left)
|
|
else:
|
|
txt += _("- {} is ready for their next payday.\n").format(member.display_name)
|
|
await ctx.send(txt)
|
|
|
|
@extendedeconomy.command(name="view")
|
|
@commands.bot_has_permissions(embed_links=True)
|
|
async def view_settings(self, ctx: commands.Context):
|
|
"""
|
|
View the current settings
|
|
"""
|
|
is_global = await bank.is_global()
|
|
if is_global and ctx.author.id not in self.bot.owner_ids:
|
|
return await ctx.send(_("You must be a bot owner to view these settings when global bank is enabled."))
|
|
view = CostMenu(ctx, self, is_global, self.cost_check)
|
|
await view.refresh()
|
|
|
|
@extendedeconomy.command(name="resetcooldown")
|
|
async def reset_payday_cooldown(self, ctx: commands.Context, *, member: discord.Member):
|
|
"""Reset the payday cooldown for a user"""
|
|
cog = self.bot.get_cog("Economy")
|
|
if not cog:
|
|
return await ctx.send(_("Economy cog is not loaded."))
|
|
if await bank.is_global() and ctx.author.id not in self.bot.owner_ids:
|
|
return await ctx.send(_("You must be a bot owner to reset cooldowns when global bank is enabled!"))
|
|
cur_time = calendar.timegm(ctx.message.created_at.utctimetuple())
|
|
if await bank.is_global():
|
|
payday_time = await cog.config.PAYDAY_TIME()
|
|
new_time = int(cur_time - payday_time)
|
|
await cog.config.user(member).next_payday.set(new_time)
|
|
else:
|
|
payday_time = await cog.config.guild(ctx.guild).PAYDAY_TIME()
|
|
new_time = int(cur_time - payday_time)
|
|
await cog.config.member(member).next_payday.set(new_time)
|
|
await ctx.send(_("Payday cooldown reset for **{}**.").format(member.display_name))
|
|
|
|
@extendedeconomy.command(name="stackpaydays", aliases=["stackpayday"])
|
|
async def stack_paydays(self, ctx: commands.Context):
|
|
"""Toggle whether payday roles stack or not"""
|
|
is_global = await bank.is_global()
|
|
if is_global:
|
|
return await ctx.send(_("This setting is not available when global bank is enabled."))
|
|
conf = self.db.get_conf(ctx.guild)
|
|
conf.stack_paydays = not conf.stack_paydays
|
|
if conf.stack_paydays:
|
|
txt = _("Payday role amounts will now stack.")
|
|
else:
|
|
txt = _("Payday role amounts will no longer stack.")
|
|
await ctx.send(txt)
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="autopaydayrole")
|
|
async def autopayday_roles(self, ctx: commands.Context, *, role: discord.Role):
|
|
"""Add/Remove auto payday roles"""
|
|
is_global = await bank.is_global()
|
|
if is_global:
|
|
txt = _("This setting is not available when global bank is enabled.")
|
|
if ctx.author.id in self.bot.owner_ids:
|
|
txt += _("\nUse {} to allow auto-claiming for for all users.").format(
|
|
f"`{ctx.clean_prefix}ecoset autopayday`"
|
|
)
|
|
return await ctx.send(txt)
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if role.id in conf.auto_claim_roles:
|
|
conf.auto_claim_roles.remove(role.id)
|
|
txt = _("This role will no longer recieve paydays automatically.")
|
|
else:
|
|
conf.auto_claim_roles.append(role.id)
|
|
txt = _("This role will now receive paydays automatically.")
|
|
await ctx.send(txt)
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="rolebonus")
|
|
async def role_bonus(self, ctx: commands.Context, role: discord.Role, bonus: float):
|
|
"""
|
|
Add/Remove Payday role bonuses
|
|
|
|
Example: `[p]ecoset rolebonus @role 0.1` - Adds a 10% bonus to the user's payday if they have the role.
|
|
|
|
To remove a bonus, set the bonus to 0.
|
|
"""
|
|
is_global = await bank.is_global()
|
|
if is_global:
|
|
return await ctx.send(_("This setting is not available when global bank is enabled."))
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if bonus <= 0:
|
|
if role.id in conf.role_bonuses:
|
|
del conf.role_bonuses[role.id]
|
|
txt = _("Role bonus removed.")
|
|
await self.save()
|
|
else:
|
|
txt = _("That role does not have a bonus.")
|
|
return await ctx.send(txt)
|
|
if role.id in conf.role_bonuses:
|
|
current = conf.role_bonuses[role.id]
|
|
if current == bonus:
|
|
return await ctx.send(_("That role already has that bonus."))
|
|
txt = _("Role bonus updated.")
|
|
else:
|
|
txt = _("Role bonus added.")
|
|
conf.role_bonuses[role.id] = bonus
|
|
await ctx.send(txt)
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="autopayday")
|
|
@commands.is_owner()
|
|
async def autopayday(self, ctx: commands.Context):
|
|
"""Toggle whether paydays are claimed automatically (Global bank)"""
|
|
is_global = await bank.is_global()
|
|
self.db.auto_payday_claim = not self.db.auto_payday_claim
|
|
if self.db.auto_payday_claim:
|
|
if is_global:
|
|
txt = _("Paydays will now be claimed automatically for all users.")
|
|
else:
|
|
txt = _("Paydays will now be claimed automatically for set roles.")
|
|
else:
|
|
txt = _("Paydays will no longer be claimed automatically.")
|
|
await ctx.send(txt)
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="autoclaimchannel")
|
|
async def auto_claim_channel(self, ctx: commands.Context, *, channel: t.Optional[discord.TextChannel] = None):
|
|
"""Set the auto claim channel"""
|
|
is_global = await bank.is_global()
|
|
if is_global:
|
|
return await ctx.send(_("There is no auto claim channel when global bank is enabled!"))
|
|
txt = _("Auto claim channel set to {}").format(channel.mention) if channel else _("Auto claim channel removed.")
|
|
if is_global:
|
|
self.db.logs.auto_claim = channel.id if channel else 0
|
|
else:
|
|
conf = self.db.get_conf(ctx.guild)
|
|
conf.logs.auto_claim = channel.id if channel else 0
|
|
await ctx.send(txt)
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="transfertax")
|
|
async def set_transfertax(self, ctx: commands.Context, tax: float):
|
|
"""
|
|
Set the transfer tax percentage as a decimal
|
|
|
|
*Example: `0.05` is for 5% tax*
|
|
|
|
- Set to 0 to disable
|
|
- Default is 0
|
|
"""
|
|
is_global = await bank.is_global()
|
|
if is_global and ctx.author.id not in self.bot.owner_ids:
|
|
return await ctx.send(_("You must be a bot owner to set the transfer tax when global bank is enabled."))
|
|
if tax < 0 or tax >= 1:
|
|
return await ctx.send(_("Invalid tax percentage. Must be between 0 and 1."))
|
|
|
|
if is_global:
|
|
self.db.transfer_tax = tax
|
|
else:
|
|
conf = self.db.get_conf(ctx.guild)
|
|
conf.transfer_tax = tax
|
|
await ctx.send(_("Transfer tax set to {}%").format(round(tax * 100, 2)))
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="taxwhitelist")
|
|
async def set_taxwhitelist(self, ctx: commands.Context, *, role: discord.Role):
|
|
"""
|
|
Add/Remove roles from the transfer tax whitelist
|
|
"""
|
|
is_global = await bank.is_global()
|
|
if is_global:
|
|
return await ctx.send(_("This setting is not available when global bank is enabled."))
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if role.id in conf.transfer_tax_whitelist:
|
|
conf.transfer_tax_whitelist.remove(role.id)
|
|
txt = _("Role removed from the transfer tax whitelist.")
|
|
else:
|
|
conf.transfer_tax_whitelist.append(role.id)
|
|
txt = _("Role added to the transfer tax whitelist.")
|
|
await ctx.send(txt)
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="mainlog")
|
|
async def set_mainlog(self, ctx: commands.Context, channel: t.Optional[discord.TextChannel] = None):
|
|
"""
|
|
Set the main log channel
|
|
"""
|
|
is_global = await bank.is_global()
|
|
if is_global and ctx.author.id not in self.bot.owner_ids:
|
|
return await ctx.send(_("You must be a bot owner to set the main log channel when global bank is enabled."))
|
|
if is_global:
|
|
current = self.db.logs.default_log_channel
|
|
else:
|
|
conf = self.db.get_conf(ctx.guild)
|
|
current = conf.logs.default_log_channel
|
|
if not channel and current:
|
|
txt = _("Removing the main log channel.")
|
|
elif not channel and not current:
|
|
return await ctx.send_help()
|
|
elif channel and current:
|
|
if channel.id == current:
|
|
return await ctx.send(_("That is already the main log channel."))
|
|
txt = _("Main log channel changed to {}").format(channel.mention)
|
|
else:
|
|
txt = _("Main log channel set to {}").format(channel.mention)
|
|
if is_global:
|
|
self.db.logs.default_log_channel = channel.id if channel else 0
|
|
else:
|
|
conf = self.db.get_conf(ctx.guild)
|
|
conf.logs.default_log_channel = channel.id if channel else 0
|
|
await ctx.send(txt)
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="eventlog")
|
|
async def set_eventlog(
|
|
self,
|
|
ctx: commands.Context,
|
|
event: str,
|
|
channel: t.Optional[discord.TextChannel] = None,
|
|
):
|
|
"""
|
|
Set an event log channel
|
|
|
|
**Events:**
|
|
- set_balance
|
|
- transfer_credits
|
|
- bank_wipe
|
|
- prune
|
|
- set_global
|
|
- payday_claim
|
|
|
|
"""
|
|
is_global = await bank.is_global()
|
|
if is_global and ctx.author.id not in self.bot.owner_ids:
|
|
return await ctx.send(_("You must be a bot owner to set an event log channel when global bank is enabled."))
|
|
if is_global:
|
|
logs = self.db.logs
|
|
else:
|
|
conf = self.db.get_conf(ctx.guild)
|
|
logs = conf.logs
|
|
valid_events = [
|
|
"set_balance",
|
|
"transfer_credits",
|
|
"bank_wipe",
|
|
"prune",
|
|
"set_global",
|
|
"payday_claim",
|
|
]
|
|
if event not in valid_events:
|
|
return await ctx.send(_("Invalid event. Must be one of: {}").format(", ".join(valid_events)))
|
|
current = getattr(logs, event)
|
|
if not current and not channel:
|
|
return await ctx.send(_("No channel set for this event."))
|
|
if current and not channel:
|
|
txt = _("Event log channel for {} removed.").format(event)
|
|
elif current and channel:
|
|
if channel.id == current:
|
|
return await ctx.send(_("That is already the event log channel for {}.").format(event))
|
|
txt = _("Event log channel for {} changed to {}").format(event, channel.mention)
|
|
else:
|
|
txt = _("Event log channel for {} set to {}").format(event, channel.mention)
|
|
if is_global:
|
|
setattr(self.db.logs, event, channel.id if channel else 0)
|
|
else:
|
|
conf = self.db.get_conf(ctx.guild)
|
|
setattr(conf.logs, event, channel.id if channel else 0)
|
|
await ctx.send(txt)
|
|
await self.save()
|
|
|
|
@extendedeconomy.command(name="deleteafter")
|
|
@commands.is_owner()
|
|
async def set_delete_after(self, ctx: commands.Context, seconds: int):
|
|
"""
|
|
Set the delete after time for cost check messages
|
|
|
|
- Set to 0 to disable (Recommended for public bots)
|
|
- Default is 0 (disabled)
|
|
"""
|
|
if not seconds:
|
|
self.db.delete_after = None
|
|
await ctx.send(_("Delete after time disabled."))
|
|
else:
|
|
self.db.delete_after = seconds
|
|
await ctx.send(_("Delete after time set to {} seconds.").format(seconds))
|
|
await self.save()
|
|
|
|
# @extendedeconomy.command(name="perguildoverride")
|
|
# @commands.is_owner()
|
|
# async def per_guild_override(self, ctx: commands.Context):
|
|
# """Toggle per guild prices when global bank is enabled"""
|
|
# self.db.per_guild_override = not self.db.per_guild_override
|
|
# if self.db.per_guild_override:
|
|
# txt = _("Per guild prices are now enabled.")
|
|
# else:
|
|
# txt = _("Per guild prices are now disabled.")
|
|
# await ctx.send(txt)
|
|
# await self.save()
|
|
|
|
@commands.command(name="addcost")
|
|
@commands.admin_or_permissions(manage_guild=True)
|
|
@commands.guild_only()
|
|
async def add_cost(
|
|
self,
|
|
ctx: commands.Context,
|
|
command: str = "",
|
|
cost: int = 0,
|
|
duration: int = 3600,
|
|
level: t.Literal["admin", "mod", "all", "user", "global"] = "all",
|
|
prompt: t.Literal["text", "reaction", "button", "silent", "notify"] = "notify",
|
|
modifier: t.Literal["static", "percent", "exponential", "linear"] = "static",
|
|
value: float = 0.0,
|
|
):
|
|
"""
|
|
Add a cost to a command
|
|
"""
|
|
if not command:
|
|
help_txt = _(
|
|
"- **cost**: The amount of currency to charge\n"
|
|
"- **duration**(`default: 3600`): The time in seconds before the cost resets\n"
|
|
"- **level**(`default: all`): The minimum permission level to apply the cost\n"
|
|
" - admin: Admins and above can use the command for free\n"
|
|
" - mod: Mods and above can use the command for free\n"
|
|
" - all: Everyone must pay the cost to use the command\n"
|
|
" - user: All users must pay the cost to use the command unless they are mod or admin\n"
|
|
" - global: The cost is applied to all users globally\n"
|
|
"- **prompt**(`default: notify`): How the user will be prompted to confirm the cost\n"
|
|
" - text: The bot will send a text message asking the user to confirm the cost with yes or no\n"
|
|
" - reaction: The bot will send a message with emoji reactions to confirm the cost\n"
|
|
" - button: The bot will send a message with buttons to confirm the cost\n"
|
|
" - silent: The bot will not prompt the user to confirm the cost\n"
|
|
" - notify: The bot will simply notify the user of the cost without asking for confirmation\n"
|
|
"- **modifier**(`default: static`): The type of cost modifier\n"
|
|
" - static: The cost is a fixed amount\n"
|
|
" - percent: The cost is a percentage of the user's balance on top of the base cost\n"
|
|
" - exponential: The cost increases exponentially based on how frequently the command is used\n"
|
|
" - Ex: `Cost = cost + (value * uses over the duration^2)`\n"
|
|
" - linear: The cost increases linearly based on how frequently the command is used\n"
|
|
" - Ex: `Cost = cost + (value * uses over the duration)`\n"
|
|
"- **value**(`default: 0.0`): The value of the cost modifier depends on the modifier type\n"
|
|
" - static: This will be 0 and does nothing\n"
|
|
" - percent: Value will be the percentage of the user's balance to add to the base cost\n"
|
|
" - exponential: Value will be the base cost multiplier\n"
|
|
" - linear: Value will be multiplied by the number of uses in the last hour to get the cost increase\n"
|
|
)
|
|
await ctx.send_help()
|
|
return await ctx.send(help_txt)
|
|
|
|
if command == "addcost":
|
|
return await ctx.send(_("You can't add a cost to the addcost command."))
|
|
|
|
is_global = await bank.is_global()
|
|
if is_global:
|
|
if ctx.author.id not in ctx.bot.owner_ids:
|
|
return await ctx.send(_("You must be a bot owner to use this command while global bank is active."))
|
|
if level != "global":
|
|
return await ctx.send(_("Global bank is active, you must use the global level."))
|
|
else:
|
|
if level == "global":
|
|
if ctx.author.id not in ctx.bot.owner_ids:
|
|
return await ctx.send(_("You must be a bot owner to use the global level."))
|
|
return await ctx.send(_("You must enable global bank to use the global level."))
|
|
|
|
command_obj: commands.Command = self.bot.get_command(command)
|
|
if not command_obj:
|
|
command_obj: app_commands.Command = self.bot.tree.get_command(command)
|
|
if not command_obj:
|
|
return await ctx.send(_("Command not found."))
|
|
if not isinstance(command_obj, app_commands.Command):
|
|
return await ctx.send(_("That is not a valid app command"))
|
|
|
|
if isinstance(command_obj, commands.commands._AlwaysAvailableCommand):
|
|
return await ctx.send(_("You can't add costs to commands that are always available!"))
|
|
if isinstance(command_obj, (commands.Command, commands.HybridCommand)):
|
|
if (command_obj.requires.privilege_level or 0) > await commands.requires.PrivilegeLevel.from_ctx(ctx):
|
|
return await ctx.send(_("You can't add costs to commands you don't have permission to run!"))
|
|
|
|
cost_obj = CommandCost(
|
|
cost=cost,
|
|
duration=duration,
|
|
level=level,
|
|
prompt=prompt,
|
|
modifier=modifier,
|
|
value=value,
|
|
)
|
|
overwrite_warning = _("This will overwrite the existing cost for this command. Continue?")
|
|
costs = self.db.command_costs if is_global else self.db.get_conf(ctx.guild).command_costs
|
|
if command in costs:
|
|
view = ConfirmView(ctx.author)
|
|
msg = await ctx.send(overwrite_warning, view=view)
|
|
await view.wait()
|
|
if not view.value:
|
|
return await msg.edit(content=_("Not adding cost."), view=None)
|
|
await msg.edit(content=_("{} cost updated.").format(command), view=None)
|
|
else:
|
|
await ctx.send(_("{} cost added.").format(command))
|
|
|
|
if is_global:
|
|
self.db.command_costs[command] = cost_obj
|
|
else:
|
|
conf = self.db.get_conf(ctx.guild)
|
|
conf.command_costs[command] = cost_obj
|
|
await self.save()
|
|
|
|
@commands.command(name="banksetrole")
|
|
@bank.is_owner_if_bank_global()
|
|
@commands.admin_or_permissions(manage_guild=True)
|
|
async def bank_set_role(self, ctx: commands.Context, role: discord.Role, creds: SetParser):
|
|
"""Set the balance of all user accounts that have a specific role
|
|
|
|
Putting + or - signs before the amount will add/remove currency on the user's bank account instead.
|
|
|
|
Examples:
|
|
- `[p]banksetrole @everyone 420` - Sets everyones balance to 420
|
|
- `[p]banksetrole @role +69` - Increases balance by 69 for everyone with the role
|
|
- `[p]banksetrole @role -42` - Decreases balance by 42 for everyone with the role
|
|
|
|
**Arguments**
|
|
|
|
- `<role>` The role to set the currency of for each user that has it.
|
|
- `<creds>` The amount of currency to set their balance to.
|
|
"""
|
|
async with ctx.typing():
|
|
if await bank.is_global():
|
|
group = bank._config._get_base_group(bank._config.USER)
|
|
else:
|
|
group = bank._config._get_base_group(bank._config.MEMBER, str(ctx.guild.id))
|
|
|
|
currency = await bank.get_currency_name(ctx.guild)
|
|
max_bal = await bank.get_max_balance(ctx.guild)
|
|
try:
|
|
default_balance = await bank.get_default_balance(ctx.guild)
|
|
except AttributeError:
|
|
default_balance = await bank.get_default_balance()
|
|
members: t.List[discord.Member] = [user for user in ctx.guild.members if role in user.roles]
|
|
if not members:
|
|
return await ctx.send(_("No users found with that role."))
|
|
|
|
users_affected = 0
|
|
total = 0
|
|
async with group.all() as accounts:
|
|
for mem in members:
|
|
uid = str(mem.id)
|
|
if uid in accounts:
|
|
wallet = accounts[uid]
|
|
else:
|
|
wallet = {"name": mem.display_name, "balance": default_balance, "created_at": 0}
|
|
accounts[uid] = wallet
|
|
|
|
match creds.operation:
|
|
case "deposit":
|
|
amount = min(max_bal - wallet["balance"], creds.sum)
|
|
case "withdraw":
|
|
amount = -min(wallet["balance"], creds.sum)
|
|
case _: # set
|
|
amount = creds.sum - wallet["balance"]
|
|
|
|
accounts[uid]["balance"] += amount
|
|
total += amount
|
|
users_affected += 1
|
|
|
|
if not users_affected:
|
|
return await ctx.send(_("No users were affected."))
|
|
if not total:
|
|
return await ctx.send(_("No balances were changed."))
|
|
grammar = _("user was") if users_affected == 1 else _("users were")
|
|
msg = _("Balances for {} updated, total change was {}.").format(
|
|
f"{users_affected} {grammar}", f"{total} {currency}"
|
|
)
|
|
await ctx.send(msg)
|
|
|
|
@commands.command(name="backpay")
|
|
@bank.is_owner_if_bank_global()
|
|
@commands.admin_or_permissions(manage_guild=True)
|
|
@commands.guild_only()
|
|
async def backpay_cmd(self, ctx: commands.Context, duration: commands.TimedeltaConverter, confirm: bool = False):
|
|
"""Calculate and award missed paydays for all members within a time period.
|
|
|
|
This will calculate how many paydays each member could have claimed within the
|
|
specified time period and award them accordingly.
|
|
|
|
By default, this command will only show a preview. Use `confirm=True` to apply changes.
|
|
|
|
Examples:
|
|
- `[p]backpay 48h` - Preview paydays missed in the last 48 hours
|
|
- `[p]backpay 7d True` - Calculate and give paydays missed in the last 7 days
|
|
|
|
**Arguments**
|
|
- `<duration>` How far back to check for missed paydays. Use time abbreviations like 1d, 12h, etc.
|
|
- `[confirm]` Set to True to actually apply the changes. Default: False (preview only)
|
|
"""
|
|
if await bank.is_global() and ctx.author.id not in self.bot.owner_ids:
|
|
return await ctx.send(_("You must be a bot owner to use this command while global bank is active."))
|
|
|
|
if duration.total_seconds() <= 0:
|
|
return await ctx.send(_("Duration must be positive!"))
|
|
|
|
async with ctx.typing():
|
|
eco_cog = self.bot.get_cog("Economy")
|
|
if not eco_cog:
|
|
return await ctx.send(_("Economy cog is not loaded."))
|
|
|
|
eco_conf: Config = eco_cog.config
|
|
is_global = await bank.is_global()
|
|
currency = await bank.get_currency_name(ctx.guild)
|
|
max_bal = await bank.get_max_balance(ctx.guild)
|
|
|
|
# Get current time and the time to look back to
|
|
current_time = calendar.timegm(datetime.now(tz=timezone.utc).utctimetuple())
|
|
|
|
# Get the payday cooldown period
|
|
if is_global:
|
|
payday_time = await eco_conf.PAYDAY_TIME()
|
|
payday_credits = await eco_conf.PAYDAY_CREDITS()
|
|
else:
|
|
payday_time = await eco_conf.guild(ctx.guild).PAYDAY_TIME()
|
|
payday_credits = await eco_conf.guild(ctx.guild).PAYDAY_CREDITS()
|
|
|
|
# Calculate total possible paydays in the lookback period
|
|
# This ensures everyone gets the same number of paydays for the same duration
|
|
total_possible_paydays = int(duration.total_seconds() // payday_time)
|
|
|
|
if total_possible_paydays <= 0:
|
|
return await ctx.send(_("The specified duration is too short for any paydays."))
|
|
|
|
# Create a list to store the report data
|
|
report_data = []
|
|
users_updated = 0
|
|
total_credits = 0
|
|
|
|
if is_global:
|
|
bankgroup = bank._config._get_base_group(bank._config.USER)
|
|
ecogroup = eco_conf._get_base_group(eco_conf.USER)
|
|
accounts: t.Dict[str, dict] = await bankgroup.all()
|
|
ecousers: t.Dict[str, dict] = await ecogroup.all()
|
|
|
|
for member in ctx.guild.members:
|
|
uid = str(member.id)
|
|
|
|
# Skip users with no bank accounts or economy data
|
|
if uid not in accounts or uid not in ecousers:
|
|
continue
|
|
|
|
# All eligible users get the same number of paydays
|
|
potential_paydays = total_possible_paydays
|
|
|
|
# Calculate amount to give (base amount * number of paydays)
|
|
amount_to_give = payday_credits * potential_paydays
|
|
|
|
# Don't exceed max balance
|
|
current_balance = accounts[uid]["balance"]
|
|
new_balance = min(current_balance + amount_to_give, max_bal)
|
|
added_amount = new_balance - current_balance
|
|
|
|
# Add to report even if no credits are added due to max balance
|
|
report_data.append(
|
|
{
|
|
"name": member.display_name,
|
|
"id": member.id,
|
|
"paydays": potential_paydays,
|
|
"amount": added_amount,
|
|
"current_balance": current_balance,
|
|
"new_balance": new_balance,
|
|
"max_hit": added_amount < amount_to_give,
|
|
}
|
|
)
|
|
|
|
# Track stats
|
|
total_credits += added_amount
|
|
users_updated += 1
|
|
|
|
# Only update the account if confirm is True
|
|
if confirm:
|
|
accounts[uid]["balance"] = new_balance
|
|
ecousers[uid]["next_payday"] = current_time
|
|
|
|
# Save changes if any and confirmation is True
|
|
if users_updated > 0 and confirm:
|
|
await bankgroup.set(accounts)
|
|
await ecogroup.set(ecousers)
|
|
|
|
else:
|
|
# Per-guild logic
|
|
conf = self.db.get_conf(ctx.guild)
|
|
bankgroup = bank._config._get_base_group(bank._config.MEMBER, str(ctx.guild.id))
|
|
ecogroup = eco_conf._get_base_group(eco_conf.MEMBER, str(ctx.guild.id))
|
|
accounts: t.Dict[str, dict] = await bankgroup.all()
|
|
ecousers: t.Dict[str, dict] = await ecogroup.all()
|
|
payday_roles: t.Dict[int, dict] = await eco_conf.all_roles()
|
|
|
|
for member in ctx.guild.members:
|
|
uid = str(member.id)
|
|
|
|
# Skip users with no bank accounts or economy data
|
|
if uid not in accounts or uid not in ecousers:
|
|
continue
|
|
|
|
# All eligible users get the same number of paydays
|
|
potential_paydays = total_possible_paydays
|
|
|
|
# Calculate per-payday amount with role bonuses
|
|
base_amount = payday_credits
|
|
for role in member.roles:
|
|
if role.id in payday_roles:
|
|
role_credits = payday_roles[role.id]["PAYDAY_CREDITS"]
|
|
if conf.stack_paydays:
|
|
base_amount += role_credits
|
|
elif role_credits > base_amount:
|
|
base_amount = role_credits
|
|
|
|
# Apply role bonus multipliers if configured
|
|
if conf.role_bonuses and any(role.id in conf.role_bonuses for role in member.roles):
|
|
highest_bonus = max(conf.role_bonuses.get(role.id, 0) for role in member.roles)
|
|
base_amount += round(base_amount * highest_bonus)
|
|
|
|
# Calculate total amount to give
|
|
amount_to_give = base_amount * potential_paydays
|
|
|
|
# Don't exceed max balance
|
|
current_balance = accounts[uid]["balance"]
|
|
new_balance = min(current_balance + amount_to_give, max_bal)
|
|
added_amount = new_balance - current_balance
|
|
|
|
# Add to report even if no credits are added due to max balance
|
|
report_data.append(
|
|
{
|
|
"name": member.display_name,
|
|
"id": member.id,
|
|
"paydays": potential_paydays,
|
|
"amount": added_amount,
|
|
"current_balance": current_balance,
|
|
"new_balance": new_balance,
|
|
"max_hit": added_amount < amount_to_give,
|
|
"payday_value": base_amount,
|
|
}
|
|
)
|
|
|
|
# Track stats
|
|
total_credits += added_amount
|
|
users_updated += 1
|
|
|
|
# Only update the account if confirm is True
|
|
if confirm:
|
|
accounts[uid]["balance"] = new_balance
|
|
ecousers[uid]["next_payday"] = current_time
|
|
|
|
# Save changes if any and confirmation is True
|
|
if users_updated > 0 and confirm:
|
|
await bankgroup.set(accounts)
|
|
await ecogroup.set(ecousers)
|
|
|
|
# Generate report
|
|
if users_updated > 0:
|
|
# Sort by amount in descending order
|
|
report_data.sort(key=lambda x: x["amount"], reverse=True)
|
|
|
|
report_lines = [
|
|
f"# Backpay Report for {humanize_timedelta(seconds=int(duration.total_seconds()))}\n",
|
|
f"Total Users: {users_updated}",
|
|
f"Total Credits: {humanize_number(total_credits)} {currency}\n",
|
|
"Details:",
|
|
]
|
|
|
|
for entry in report_data:
|
|
max_note = " (Max balance hit)" if entry.get("max_hit") else ""
|
|
per_payday = (
|
|
f" ({humanize_number(entry.get('payday_value', payday_credits))}/payday)"
|
|
if "payday_value" in entry
|
|
else ""
|
|
)
|
|
report_lines.append(
|
|
f"{entry['name']} (ID: {entry['id']}): "
|
|
f"{humanize_number(entry['amount'])} {currency} for {entry['paydays']} paydays{per_payday}{max_note}"
|
|
)
|
|
|
|
report_text = "\n".join(report_lines)
|
|
report_file = discord.File(io.StringIO(report_text), filename=f"backpay_report_{ctx.guild.id}.txt")
|
|
|
|
if confirm:
|
|
msg = _(
|
|
"Backpay complete for {duration}!\n{users} users received a total of {credits} {currency}."
|
|
).format(
|
|
duration=humanize_timedelta(seconds=int(duration.total_seconds())),
|
|
users=humanize_number(users_updated),
|
|
credits=humanize_number(total_credits),
|
|
currency=currency,
|
|
)
|
|
await ctx.send(msg, file=report_file)
|
|
else:
|
|
msg = _(
|
|
"Backpay preview for {duration}:\n{users} users would receive a total of {credits} {currency}.\n"
|
|
"Run with `confirm=True` to apply these changes."
|
|
).format(
|
|
duration=humanize_timedelta(seconds=int(duration.total_seconds())),
|
|
users=humanize_number(users_updated),
|
|
credits=humanize_number(total_credits),
|
|
currency=currency,
|
|
)
|
|
await ctx.send(msg, file=report_file)
|
|
else:
|
|
await ctx.send(_("No users were eligible for backpay in that time period."))
|