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

369 lines
14 KiB
Python

import math
from datetime import datetime, timedelta
from io import StringIO
import discord
from redbot.core import bank, commands
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import humanize_number, text_to_file
from ..abc import MixinMeta
from ..common.confirm_view import ConfirmView
from ..common.models import User
_ = Translator("BankDecay", __file__)
@cog_i18n(_)
class Admin(MixinMeta):
@commands.group(aliases=["bdecay"])
@commands.admin_or_permissions(manage_guild=True)
@commands.guild_only()
async def bankdecay(self, ctx: commands.Context):
"""
Setup economy credit decay for your server
"""
pass
@bankdecay.command(name="view")
@commands.bot_has_permissions(embed_links=True)
async def view_settings(self, ctx: commands.Context):
"""View Bank Decay Settings"""
conf = self.db.get_conf(ctx.guild)
expired = 0
active = 0
left_server = 0
for uid, user in conf.users.items():
member = ctx.guild.get_member(uid)
if not member:
left_server += 1
elif user.last_active + timedelta(days=conf.inactive_days) < datetime.now():
expired += 1
else:
active += 1
ignored_roles = [f"<@&{i}>" for i in conf.ignored_roles]
log_channel = (
ctx.guild.get_channel(conf.log_channel) if ctx.guild.get_channel(conf.log_channel) else _("Not Set")
)
now = datetime.now()
next_midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
next_run = f"<t:{round(next_midnight.timestamp())}:R>"
txt = _(
"`Decay Enabled: `{}\n"
"`Inactive Days: `{}\n"
"`Percent Decay: `{}\n"
"`Saved Users: `{}\n"
"`Active Users: `{}\n"
"`Expired Users: `{}\n"
"`Stale Users: `{}\n"
"`Total Decayed: `{}\n"
"`Log Channel: `{}\n"
).format(
conf.enabled,
conf.inactive_days,
round(conf.percent_decay * 100),
humanize_number(len(conf.users)),
humanize_number(active),
humanize_number(expired),
humanize_number(left_server),
humanize_number(conf.total_decayed),
log_channel,
)
if conf.enabled:
txt += _("`Next Runtime: `{}\n").format(next_run)
if ignored_roles:
joined = ", ".join(ignored_roles)
txt += _("**Ignored Roles**\n") + joined
embed = discord.Embed(
title=_("BankDecay Settings"),
description=txt,
color=ctx.author.color,
)
await ctx.send(embed=embed)
@bankdecay.command(name="toggle")
async def toggle_decay(self, ctx: commands.Context):
"""
Toggle the bank decay feature on or off.
"""
if await bank.is_global():
await ctx.send(_("This command is not available when using global bank."))
return
conf = self.db.get_conf(ctx.guild)
conf.enabled = not conf.enabled
await ctx.send(_("Bank decay has been {}.").format(_("enabled") if conf.enabled else _("disabled")))
await self.save()
@bankdecay.command(name="setdays")
async def set_inactive_days(self, ctx: commands.Context, days: commands.positive_int):
"""
Set the number of inactive days before decay starts.
"""
conf = self.db.get_conf(ctx.guild)
conf.inactive_days = days
await ctx.send(_("Inactive days set to {}.").format(days))
await self.save()
@bankdecay.command(name="setpercent")
async def set_percent_decay(self, ctx: commands.Context, percent: float):
"""
Set the percentage of decay that occurs after the inactive period.
**Example**
If decay is 5%, then after the set days of inactivity they will lose 5% of their balance every day.
"""
if not 0 <= percent <= 1:
await ctx.send(_("Percent decay must be between 0 and 1."))
return
conf = self.db.get_conf(ctx.guild)
conf.percent_decay = percent
await ctx.send(_("Percent decay set to {}%.").format(round(percent * 100)))
await self.save()
@bankdecay.command(name="resettotal")
async def reset_total_decayed(self, ctx: commands.Context):
"""
Reset the total amount decayed to zero.
"""
conf = self.db.get_conf(ctx.guild)
conf.total_decayed = 0
await ctx.send(_("Total decayed amount has been reset to 0."))
await self.save()
@bankdecay.command(name="decaynow")
async def decay_now(self, ctx: commands.Context, force: bool = False):
"""
Run a decay cycle on this server right now
"""
if await bank.is_global():
await ctx.send(_("This command is not available when using global bank."))
return
conf = self.db.get_conf(ctx.guild)
if not conf.enabled:
txt = _("The decay system is currently disabled!")
return await ctx.send(txt)
async with ctx.typing():
currency = await bank.get_currency_name(ctx.guild)
if not force:
decayed = await self.decay_guild(ctx.guild, check_only=True)
if not decayed:
txt = _("There were no users affected by the decay cycle")
return await ctx.send(txt)
grammar = _("account") if len(decayed) == 1 else _("accounts")
txt = _("Are you sure you want to decay {} for a total of {}?").format(
f"**{humanize_number(len(decayed))}** {grammar}",
f"**{humanize_number(sum(decayed.values()))}** {currency}",
)
view = ConfirmView(ctx.author)
msg = await ctx.send(txt, view=view)
await view.wait()
if not view.value:
txt = _("Decay cycle cancelled")
return await msg.edit(content=txt, view=None)
txt = _("Decaying user accounts, one moment...")
await msg.edit(content=txt, view=None)
else:
txt = _("Decaying user accounts, one moment...")
msg = await ctx.send(txt)
decayed = await self.decay_guild(ctx.guild)
txt = _("User accounts have been decayed!\n- Users Affected: {}\n- Total {} Decayed: {}").format(
humanize_number(len(decayed)), currency, humanize_number(sum(decayed.values()))
)
await msg.edit(content=txt)
await self.save()
@bankdecay.command(name="getexpired")
async def get_expired_users(self, ctx: commands.Context):
"""Get a list of users who are currently expired and how much they will lose if decayed"""
if await bank.is_global():
await ctx.send(_("This command is not available when using global bank."))
return
async with ctx.typing():
decayed = await self.decay_guild(ctx.guild, check_only=True)
if not decayed:
txt = _("There were no users that would be affected by the decay cycle")
return await ctx.send(txt)
grammar = _("account") if len(decayed) == 1 else _("accounts")
txt = _("This would decay {} for a total of {}").format(
f"**{humanize_number(len(decayed))}** {grammar}",
f"**{humanize_number(sum(decayed.values()))}** credits",
)
# Create a text file with the list of users and how much they will lose
buffer = StringIO()
for user, amount in sorted(decayed.items(), key=lambda x: x[1], reverse=True):
buffer.write(f"{user}: {amount}\n")
buffer.seek(0)
file = text_to_file(buffer.getvalue(), filename="expired_users.txt")
await ctx.send(txt, file=file)
@bankdecay.command(name="cleanup")
async def cleanup(self, ctx: commands.Context, confirm: bool):
"""
Remove users from the config that are no longer in the server or have no balance
"""
if await bank.is_global():
await ctx.send(_("This command is not available when using global bank."))
return
if not confirm:
txt = _("Not removing users from the config")
return await ctx.send(txt)
conf = self.db.get_conf(ctx.guild)
global_bank = await bank.is_global()
cleaned = 0
for uid in conf.users.copy():
member = ctx.guild.get_member(uid)
if not member:
del conf.users[uid]
cleaned += 1
elif not global_bank and await bank.get_balance(member) == 0:
del conf.users[uid]
cleaned += 1
if not cleaned:
txt = _("No users were removed from the config.")
return await ctx.send(txt)
grammar = _("user") if cleaned == 1 else _("users")
txt = _("Removed {} from the config.").format(f"{cleaned} {grammar}")
await ctx.send(txt)
await self.save()
@bankdecay.command(name="initialize")
async def initialize_guild(self, ctx: commands.Context, as_expired: bool):
"""
Initialize the server and add every member to the config.
**Arguments**
- as_expired: (t/f) if True, initialize users as already expired
"""
if await bank.is_global():
await ctx.send(_("This command is not available when using global bank."))
return
async with ctx.typing():
initialized = 0
conf = self.db.get_conf(ctx.guild)
for member in ctx.guild.members:
if member.bot: # Skip bots
continue
if member.id in conf.users:
continue
user = conf.get_user(member) # This will add the member to the config if not already present
initialized += 1
if as_expired:
user.last_active = user.last_active - timedelta(days=conf.inactive_days + 1)
grammar = _("member") if initialized == 1 else _("members")
await ctx.send(_("Server initialized! {} added to the config.").format(f"{initialized} {grammar}"))
await self.save()
@bankdecay.command(name="seen")
async def last_seen(self, ctx: commands.Context, *, user: discord.Member | int):
"""
Check when a user was last active (if at all)
"""
conf = self.db.get_conf(ctx.guild)
uid = user if isinstance(user, int) else user.id
if uid not in conf.users:
txt = _("This user is not in the config yet!")
return await ctx.send(txt)
user: User = conf.get_user(uid)
txt = _("User was last seen {}").format(f"{user.seen_f} ({user.seen_r})")
await ctx.send(txt)
@bankdecay.command(name="ignorerole")
async def ignore_role(self, ctx: commands.Context, *, role: discord.Role):
"""
Add/Remove a role from the ignore list
Users with an ignored role will not have their balance decay
"""
conf = self.db.get_conf(ctx.guild)
if role.id in conf.ignored_roles:
conf.ignored_roles.remove(role.id)
txt = _("Role removed from the ignore list.")
else:
conf.ignored_roles.append(role.id)
txt = _("Role added to the ignore list.")
await ctx.send(txt)
await self.save()
@bankdecay.command(name="logchannel")
async def set_log_channel(self, ctx: commands.Context, *, channel: discord.TextChannel):
"""
Set the log channel, each time the decay cycle runs this will be updated
"""
conf = self.db.get_conf(ctx.guild)
conf.log_channel = channel.id
await ctx.send(_("Log channel has been set!"))
await self.save()
@bankdecay.command(name="bulkaddpercent")
async def bulk_add_percent(self, ctx: commands.Context, percent: int, confirm: bool):
"""
Add a percentage to all member balances.
Accidentally decayed too many credits? Bulk add to every user's balance in the server based on a percentage of their current balance.
"""
if await bank.is_global():
await ctx.send(_("This command is not available when using global bank."))
return
if not confirm:
txt = _("Not adding credits to users")
return await ctx.send(txt)
if percent < 1:
txt = _("Percent must be greater than 1!")
return await ctx.send(txt)
async with ctx.typing():
refunded = 0
ratio = percent / 100
conf = self.db.get_conf(ctx.guild)
users = [ctx.guild.get_member(int(i)) for i in conf.users if ctx.guild.get_member(int(i))]
for user in users:
bal = await bank.get_balance(user)
to_give = math.ceil(bal * ratio)
await bank.set_balance(user, bal + to_give)
refunded += to_give
await ctx.send(_("Credits added: {}").format(humanize_number(refunded)))
@bankdecay.command(name="bulkrempercent")
async def bulk_rem_percent(self, ctx: commands.Context, percent: int, confirm: bool):
"""
Remove a percentage from all member balances.
Accidentally refunded too many credits with bulkaddpercent? Bulk remove from every user's balance in the server based on a percentage of their current balance.
"""
if await bank.is_global():
await ctx.send(_("This command is not available when using global bank."))
return
if not confirm:
txt = _("Not removing credits from users")
return await ctx.send(txt)
if percent < 1:
txt = _("Percent must be greater than 1!")
return await ctx.send(txt)
async with ctx.typing():
taken = 0
ratio = percent / 100
conf = self.db.get_conf(ctx.guild)
users = [ctx.guild.get_member(int(i)) for i in conf.users if ctx.guild.get_member(int(i))]
for user in users:
bal = await bank.get_balance(user)
to_take = math.ceil(bal * ratio)
await bank.withdraw_credits(user, to_take)
taken += to_take
await ctx.send(_("Credits removed: {}").format(humanize_number(taken)))