Ruby-Cogs/extendedeconomy/common/listeners.py
2025-05-23 02:30:00 -04:00

247 lines
11 KiB
Python

import logging
import typing as t
from contextlib import suppress
from datetime import datetime
import discord
from discord.ext import tasks
from redbot.core import bank, commands, errors
from redbot.core.i18n import Translator
from redbot.core.utils.chat_formatting import humanize_number
from ..abc import MixinMeta
from ..common.utils import ctx_to_id, has_cost_check
log = logging.getLogger("red.vrt.extendedeconomy.listeners")
_ = Translator("ExtendedEconomy", __file__)
class Listeners(MixinMeta):
def __init__(self):
super().__init__()
self.payloads: t.Dict[int, t.List[discord.Embed]] = {}
@commands.Cog.listener()
async def on_command_error(self, ctx: commands.Context, error: Exception, *args, **kwargs):
if not ctx.command:
return
# log.debug(f"Command error in '{ctx.command.qualified_name}' for {ctx.author.name}:({type(error)}) {error}")
runtime_id = ctx_to_id(ctx)
if runtime_id not in self.charged:
# User wasn't charged for this command
return
# We need to refund the user if the command failed
amount = self.charged.pop(runtime_id)
try:
await bank.deposit_credits(ctx.author, amount)
except errors.BalanceTooHigh as e:
await bank.set_balance(ctx.author, e.max_balance)
cmd = ctx.command.qualified_name
log.info(f"{ctx.author.name} has been refunded {amount} since the '{cmd}' command failed.")
txt = _("You have been refunded since this command failed.")
await ctx.send(txt)
@commands.Cog.listener()
async def on_command_completion(self, ctx: commands.Context):
if not ctx.command:
return
self.charged.pop(ctx_to_id(ctx), None)
@commands.Cog.listener()
async def on_cog_add(self, cog: commands.Cog):
if cog.qualified_name == "BankEvents":
log.debug("BankEvents cog loaded 'after' ExtendedEconomy, overriding payday command.")
payday: commands.Command = self.bot.get_command("payday")
if payday:
self.payday_callback = payday.callback
payday.callback = self._extendedeconomy_payday_override.callback
if cog.qualified_name in self.checks:
return
for cmd in cog.walk_app_commands():
if isinstance(cmd, discord.app_commands.Group):
continue
if has_cost_check(cmd):
continue
cmd.add_check(self.cost_check)
self.checks.add(cog.qualified_name)
@commands.Cog.listener()
async def on_cog_remove(self, cog: commands.Cog):
self.checks.discard(cog.qualified_name)
async def log_event(self, event: str, payload: t.NamedTuple):
is_global = await bank.is_global()
guild = (
payload.member.guild
if event == "payday_claim" and isinstance(payload.member, discord.Member)
else getattr(payload, "guild", None)
)
if not is_global and not guild:
log.error(f"Guild is None for non-global bank event: {event}\n{payload}")
return
logs = self.db.logs if is_global else self.db.get_conf(guild).logs
channel_id = getattr(logs, event, 0) or logs.default_log_channel
if not channel_id:
return
channel: discord.TextChannel = self.bot.get_channel(channel_id)
if not channel:
return
event_map = {
"set_balance": _("Set Balance"),
"transfer_credits": _("Transfer Credits"),
"bank_wipe": _("Bank Wipe"),
"prune": _("Prune Accounts"),
"set_global": _("Set Global"),
"payday_claim": _("Payday Claim"),
}
currency = await bank.get_currency_name(guild)
title = _("Bank Event: {}").format(event_map[event])
color = await self.bot.get_embed_color(channel)
embed = discord.Embed(title=title, color=color, timestamp=datetime.now())
if event == "set_balance":
embed.add_field(name=_("Recipient"), value=f"{payload.recipient.mention}\n`{payload.recipient.id}`")
embed.add_field(name=_("Old Balance"), value=humanize_number(payload.recipient_old_balance))
embed.add_field(name=_("New Balance"), value=humanize_number(payload.recipient_new_balance))
if guild and is_global:
embed.add_field(name=_("Guild"), value=guild.name)
elif event == "transfer_credits":
embed.add_field(name=_("Sender"), value=f"{payload.sender.mention}\n`{payload.sender.id}`")
embed.add_field(name=_("Recipient"), value=f"{payload.recipient.mention}\n`{payload.recipient.id}`")
embed.add_field(name=_("Transfer Amount"), value=f"{humanize_number(payload.transfer_amount)} {currency}")
if guild and is_global:
embed.add_field(name=_("Guild"), value=guild.name)
elif event == "prune":
if payload.user_id:
embed.add_field(name=_("User ID"), value=payload.user_id)
else:
embed.add_field(name=_("Pruned Users"), value=humanize_number(len(payload.pruned_users)))
if guild and is_global:
embed.add_field(name=_("Guild"), value=guild.name)
elif event == "payday_claim":
embed.add_field(name=_("Recipient"), value=f"{payload.member.mention}\n`{payload.member.id}`")
embed.add_field(name=_("Amount"), value=f"{humanize_number(payload.amount)} {currency}")
embed.add_field(name=_("Old Balance"), value=humanize_number(payload.old_balance))
embed.add_field(name=_("New Balance"), value=humanize_number(payload.new_balance))
if is_global:
embed.add_field(name=_("Guild"), value=guild.name if guild else _("Unknown"))
else:
embed.add_field(name=_("Channel"), value=payload.channel.mention)
if message := getattr(payload, "message", None):
embed.add_field(name=_("Message"), value=f"[Jump]({message.jump_url})")
else:
log.error(f"Unknown event type: {event}")
return
if channel.id in self.payloads:
self.payloads[channel.id].append(embed)
else:
self.payloads[channel.id] = [embed]
@commands.Cog.listener()
async def on_red_bank_set_balance(self, payload: t.NamedTuple):
"""Payload attributes:
- recipient: Union[discord.Member, discord.User]
- guild: Union[discord.Guild, None]
- recipient_old_balance: int
- recipient_new_balance: int
"""
await self.log_event("set_balance", payload)
@commands.Cog.listener()
async def on_red_bank_transfer_credits(self, payload: t.NamedTuple):
"""Payload attributes:
- sender: Union[discord.Member, discord.User]
- recipient: Union[discord.Member, discord.User]
- guild: Union[discord.Guild, None]
- transfer_amount: int
- sender_new_balance: int
- recipient_new_balance: int
"""
await self.log_event("transfer_credits", payload)
@commands.Cog.listener()
async def on_red_bank_wipe(self, scope: t.Optional[int] = None):
"""scope: int (-1 for global, None for all members, guild_id for server bank)"""
if scope == -1 or scope is None:
log_channel_id = self.db.logs.bank_wipe or self.db.logs.default_log_channel
txt = _("Global bank has been wiped!")
elif scope is None:
log_channel_id = self.db.logs.bank_wipe or self.db.logs.default_log_channel
txt = _("All bank accounts for all guilds have been wiped!")
else:
# Scope is a guild ID
guild: discord.Guild = self.bot.get_guild(scope)
if not guild:
return
conf = self.db.get_conf(guild)
log_channel_id = conf.logs.bank_wipe or conf.logs.default_log_channel
txt = _("Bank accounts have been wiped!")
if not log_channel_id:
return
log_channel: discord.TextChannel = self.bot.get_channel(log_channel_id)
if not log_channel:
return
embed = discord.Embed(
title=_("Bank Wipe"),
description=txt,
color=await self.bot.get_embed_color(log_channel),
)
with suppress(discord.HTTPException, discord.Forbidden):
await log_channel.send(embed=embed)
@commands.Cog.listener()
async def on_red_bank_prune(self, payload: t.NamedTuple):
"""Payload attributes:
- guild: Union[discord.Guild, None]
- user_id: Union[int, None]
- scope: int (1 for global, 2 for server, 3 for user)
- pruned_users: list[int(user_id)] or dict[int(guild_id), list[int(user_id)]]
"""
await self.log_event("prune", payload)
@commands.Cog.listener()
async def on_red_bank_set_global(self, is_global: bool):
"""is_global: True if global bank, False if server bank"""
txt = _("Bank has been set to Global!") if is_global else _("Bank has been set to per-server!")
log_channel_id = self.db.logs.set_global or self.db.logs.default_log_channel
if not log_channel_id:
return
log_channel: discord.TextChannel = self.bot.get_channel(log_channel_id)
if not log_channel:
return
embed = discord.Embed(
title=_("Set Global Bank"),
description=txt,
color=await self.bot.get_embed_color(log_channel),
)
# with suppress(discord.HTTPException, discord.Forbidden):
await log_channel.send(embed=embed)
@commands.Cog.listener()
async def on_red_economy_payday_claim(self, payload: t.NamedTuple):
"""Payload attributes:
- member: discord.Member
- channel: Union[discord.TextChannel, discord.Thread, discord.ForumChannel]
- amount: int
- old_balance: int
- new_balance: int
"""
await self.log_event("payday_claim", payload)
@tasks.loop(seconds=4)
async def send_payloads(self):
"""Send embeds in chunks to avoid rate limits"""
if not self.payloads:
return
tmp = self.payloads.copy()
self.payloads.clear()
for channel_id, embeds in tmp.items():
channel = self.bot.get_channel(channel_id)
if not channel:
continue
# Group the embeds into 5 per message
for i in range(0, len(embeds), 5):
chunk = embeds[i : i + 5]
with suppress(discord.HTTPException, discord.Forbidden):
await channel.send(embeds=chunk)