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

214 lines
7.4 KiB
Python

import asyncio
import logging
import math
import typing as t
from datetime import datetime, timedelta
from io import StringIO
import discord
from redbot.core import Config, bank, commands
from redbot.core.bot import Red
from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import humanize_number, text_to_file
from .abc import CompositeMetaClass
from .commands.admin import Admin
from .common.listeners import Listeners
from .common.models import DB
from .common.scheduler import scheduler
log = logging.getLogger("red.vrt.bankdecay")
RequestType = t.Literal["discord_deleted_user", "owner", "user", "user_strict"]
_ = Translator("BankDecay", __file__)
# redgettext -D main.py commands/admin.py --command-docstring
@cog_i18n(_)
class BankDecay(Admin, Listeners, commands.Cog, metaclass=CompositeMetaClass):
"""
Economy decay!
Periodically reduces users' red currency based on inactivity, encouraging engagement.
Server admins can configure decay parameters, view settings, and manually trigger decay cycles.
User activity is tracked via messages and reactions.
"""
__author__ = "[vertyco](https://github.com/vertyco/vrt-cogs)"
__version__ = "0.3.12"
def __init__(self, bot: Red):
super().__init__()
self.bot = bot
self.config = Config.get_conf(self, 117, force_registration=True)
self.config.register_global(db={})
self.db: DB = DB()
self.saving = False
async def cog_load(self) -> None:
scheduler.start()
scheduler.remove_all_jobs()
asyncio.create_task(self.initialize())
async def cog_unload(self) -> None:
scheduler.remove_all_jobs()
scheduler.shutdown(wait=False)
async def initialize(self) -> None:
await self.bot.wait_until_red_ready()
data = await self.config.db()
self.db = await asyncio.to_thread(DB.model_validate, data)
log.info("Config loaded")
await self.start_jobs()
async def start_jobs(self):
kwargs = {
"func": self.autodecay_guilds,
"trigger": "cron",
"minute": 0,
"hour": 0,
"id": "BankDecay.autodecay_guilds",
"replace_existing": True,
"misfire_grace_time": 3600, # 1 hour grace time for missed job
}
# If it has been more than 24 hours since the last run, schedule it to run now
if self.db.last_run is not None and (datetime.now() - self.db.last_run) > timedelta(hours=24):
kwargs["next_run_time"] = datetime.now() + timedelta(seconds=5)
# Schedule decay job
scheduler.add_job(**kwargs)
async def autodecay_guilds(self):
if await bank.is_global():
log.error("This cog cannot be used with a global bank!")
return
log.info("Running decay_guilds!")
total_affected = 0
total_decayed = 0
for guild_id in self.db.configs.copy():
guild = self.bot.get_guild(guild_id)
if not guild:
# Remove guids that the bot is no longer a part of
del self.db.configs[guild_id]
continue
decayed = await self.decay_guild(guild)
total_affected += len(decayed)
total_decayed += sum(decayed.values())
if total_affected or total_decayed:
log.info(f"Decayed {total_affected} users balances for a total of {total_decayed} credits!")
self.db.last_run = datetime.now()
await self.save()
async def decay_guild(self, guild: discord.Guild, check_only: bool = False) -> t.Dict[str, int]:
now = datetime.now()
conf = self.db.get_conf(guild)
if not conf.enabled and not check_only:
return {}
_bank_members = await bank._config.all_members(guild)
bank_members: t.Dict[int, int] = {int(k): v["balance"] for k, v in _bank_members.items()}
# Decayed users: dict[username, amount]
decayed: t.Dict[str, int] = {}
uids = [i for i in conf.users]
for user_id in uids:
user = guild.get_member(user_id)
if not user:
# User no longer in guild
continue
if any(r.id in conf.ignored_roles for r in user.roles):
# Don't decay user balances with roles in the ignore list
continue
last_active = conf.get_user(user).last_active
delta = now - last_active
if delta.days <= conf.inactive_days:
continue
bal = bank_members.get(user_id)
# bal = await bank.get_balance(user)
if not bal:
continue
credits_to_remove = math.ceil(bal * conf.percent_decay)
new_bal = bal - credits_to_remove
if not check_only:
await bank.set_balance(user, new_bal)
decayed[user.name] = credits_to_remove
if check_only:
return decayed
conf.total_decayed += sum(decayed.values())
log.info(f"Decayed guild {guild.name}.\nUsers decayed: {len(decayed)}\nTotal: {sum(decayed.values())}")
log_channel = guild.get_channel(conf.log_channel)
if not log_channel:
return decayed
if not log_channel.permissions_for(guild.me).embed_links:
return decayed
title = _("Bank Decay Cycle")
if decayed:
txt = _("- User Balances Decayed: {}\n- Total Amount Decayed: {}").format(
f"`{humanize_number(len(decayed))}`", f"`{humanize_number(sum(decayed.values()))}`"
)
color = discord.Color.yellow()
else:
txt = _("No user balances were decayed during this cycle.")
color = discord.Color.blue()
embed = discord.Embed(
title=title,
description=txt,
color=color,
timestamp=datetime.now(),
)
# 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")
file = text_to_file(buffer.getvalue(), filename="decay.txt")
perms = [
log_channel.permissions_for(guild.me).attach_files,
log_channel.permissions_for(guild.me).embed_links,
]
if not any(perms):
return decayed
try:
if perms[0] and perms[1]:
await log_channel.send(embed=embed, file=file)
elif perms[1]:
await log_channel.send(embed=embed)
except Exception as e:
log.error(f"Failed to send decay log to {log_channel.name} in {guild.name}", exc_info=e)
return decayed
async def save(self) -> None:
if self.saving:
return
try:
self.saving = True
dump = self.db.model_dump(mode="json")
await self.config.db.set(dump)
except Exception as e:
log.exception("Failed to save config", exc_info=e)
finally:
self.saving = False
def format_help_for_context(self, ctx: commands.Context):
helpcmd = super().format_help_for_context(ctx)
txt = "Version: {}\nAuthor: {}".format(self.__version__, self.__author__)
return f"{helpcmd}\n\n{txt}"
async def red_delete_data_for_user(self, *, requester: RequestType, user_id: int):
"""No data to delete"""