Ruby-Cogs/extendedeconomy/commands/admin.py
2025-05-23 02:30:00 -04:00

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."))