Add automatic leaderboard sync task and enhance user credit retrieval in Leaderboard cog
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run

Implement a background task for automatic leaderboard synchronization every 6 hours, improving data consistency. Update user credit retrieval to first check the current server's balance before aggregating from other servers, ensuring accurate total credits. Enhance error handling and logging for better debugging and user feedback during API interactions.
This commit is contained in:
Valerie 2025-05-26 05:55:32 -04:00
parent bc602b73bf
commit 02d67baeef

View file

@ -82,6 +82,9 @@ class Leaderboard(commands.Cog):
self.api_base_url = "https://ruby.valerie.lol/api" self.api_base_url = "https://ruby.valerie.lol/api"
self.admin_secret = None self.admin_secret = None
# Add task for auto-sync
self.auto_sync_task = None
default_guild = { default_guild = {
"min_message_length": 5, "min_message_length": 5,
"cooldown": 60, "cooldown": 60,
@ -95,7 +98,7 @@ class Leaderboard(commands.Cog):
self.config.register_user(**default_user) self.config.register_user(**default_user)
async def initialize(self): async def initialize(self):
"""Initialize optional API connection.""" """Initialize optional API connection and start auto-sync task."""
try: try:
self.admin_secret = await self.bot.get_shared_api_tokens("ruby_api") self.admin_secret = await self.bot.get_shared_api_tokens("ruby_api")
if self.admin_secret.get("admin_secret"): if self.admin_secret.get("admin_secret"):
@ -107,9 +110,56 @@ class Leaderboard(commands.Cog):
) as resp: ) as resp:
if resp.status != 200: if resp.status != 200:
log.error(f"Failed to connect to API: Status {resp.status}") log.error(f"Failed to connect to API: Status {resp.status}")
else:
# Start auto-sync task if API connection successful
self.auto_sync_task = self.bot.loop.create_task(self._auto_sync_task())
log.info("Started automatic leaderboard sync task")
except Exception as e: except Exception as e:
log.error(f"Failed to initialize API connection: {e}") log.error(f"Failed to initialize API connection: {e}")
async def _auto_sync_task(self):
"""Task to automatically sync leaderboard every 6 hours."""
await self.bot.wait_until_ready()
while True:
try:
await self._perform_full_sync()
log.info("Completed automatic leaderboard sync")
await asyncio.sleep(21600) # 6 hours in seconds
except asyncio.CancelledError:
break
except Exception as e:
log.error(f"Error in automatic sync task: {e}")
await asyncio.sleep(300) # Wait 5 minutes before retrying on error
async def _perform_full_sync(self) -> tuple[int, int]:
"""Perform a full sync of all users to the API.
Returns tuple of (success_count, fail_count)"""
success_count = 0
fail_count = 0
processed_users = set()
for guild in self.bot.guilds:
for member in guild.members:
if member.bot or member.id in processed_users:
continue
try:
# Get total credits across all guilds
credits = await self.get_user_credits(member)
# Update API
if await self._try_api_update(str(member.id), str(member), credits):
success_count += 1
else:
fail_count += 1
processed_users.add(member.id)
except Exception as e:
log.error(f"Error syncing user {member} ({member.id}): {e}")
fail_count += 1
return success_count, fail_count
async def cog_load(self): async def cog_load(self):
"""Initialize the cog when it's loaded.""" """Initialize the cog when it's loaded."""
await self.initialize() await self.initialize()
@ -118,54 +168,60 @@ class Leaderboard(commands.Cog):
"""Get a user's total credits from all servers.""" """Get a user's total credits from all servers."""
total_credits = 0 total_credits = 0
# Get credits from all servers the user is in try:
for guild in self.bot.guilds: # Get credits from the user's current server first
if user in guild.members: # Only check servers where the user is a member total_credits = await bank.get_balance(user)
try:
credits = await bank.get_balance(user) # Then add credits from all other servers where they are a member
total_credits += credits for guild in self.bot.guilds:
except Exception as e: if guild != user.guild and user in guild.members: # Skip current guild since we already got those credits
log.error(f"Error getting bank balance for {user} in {guild}: {e}") try:
continue guild_credits = await bank.get_balance(user, guild)
total_credits += guild_credits
except Exception as e:
log.error(f"Error getting bank balance for {user} in {guild}: {e}")
continue
except Exception as e:
log.error(f"Error getting total credits for {user}: {e}")
return total_credits return total_credits
async def get_all_balances(self) -> List[dict]: async def get_all_balances(self) -> List[dict]:
"""Get all users' credit balances across all servers.""" """Get all users' credit balances across all servers."""
all_users = {} all_users = {}
min_credits = 10000 # Minimum credits to show on leaderboard min_credits = 10000 # Minimum credits to show on leaderboard - matches API's MIN_CREDITS
# Collect all unique members and sum their balances across all guilds # First get all unique members across all guilds
all_members = set()
for guild in self.bot.guilds: for guild in self.bot.guilds:
for member in guild.members: for member in guild.members:
if member.bot: if not member.bot:
continue all_members.add(member)
# Skip opted-out users
if await self.config.user(member).opted_out():
continue
try:
# Get balance for this guild
credits = await bank.get_balance(member)
if member.id not in all_users:
all_users[member.id] = {
"userId": str(member.id),
"username": str(member),
"points": credits
}
else:
# Add this guild's balance to user's total
all_users[member.id]["points"] += credits
except Exception as e:
log.error(f"Error getting balance for {member} in {guild}: {e}")
continue
# Filter out users with less than minimum credits and sort by total # Now get total credits for each unique member
for member in all_members:
# Skip opted-out users
if await self.config.user(member).opted_out():
continue
try:
total_credits = await self.get_user_credits(member)
if total_credits >= min_credits:
all_users[member.id] = {
"userId": str(member.id),
"username": str(member),
"points": total_credits
}
except Exception as e:
log.error(f"Error getting balance for {member}: {e}")
continue
# Sort by total credits
sorted_users = sorted( sorted_users = sorted(
[user for user in all_users.values() if user["points"] >= min_credits], all_users.values(),
key=lambda x: x["points"], key=lambda x: x["points"],
reverse=True reverse=True
) )
@ -180,6 +236,9 @@ class Leaderboard(commands.Cog):
try: try:
if credits < 0: if credits < 0:
credits = 0 # API doesn't accept negative values credits = 0 # API doesn't accept negative values
# Get opt-out status
opted_out = await self.config.user_from_id(int(user_id)).opted_out()
async with self.session.post( async with self.session.post(
f"{self.api_base_url}/leaderboard", f"{self.api_base_url}/leaderboard",
@ -190,13 +249,14 @@ class Leaderboard(commands.Cog):
json={ json={
"userId": str(user_id), "userId": str(user_id),
"username": str(username), "username": str(username),
"points": credits "points": credits,
"optedOut": opted_out
} }
) as resp: ) as resp:
if resp.status == 200: if resp.status == 200:
data = await resp.json() data = await resp.json()
if data.get("success"): if data.get("success"):
log.info(f"Updated leaderboard for {username}: {credits} credits") log.info(f"Updated leaderboard for {username}: {credits} credits (opted_out: {opted_out})")
return True return True
else: else:
log.error(f"API update failed: {data.get('error', 'Unknown error')}") log.error(f"API update failed: {data.get('error', 'Unknown error')}")
@ -293,6 +353,11 @@ class Leaderboard(commands.Cog):
return return
await user_settings.opted_out.set(True) await user_settings.opted_out.set(True)
# Update API with new opt-out status
credits = await self.get_user_credits(ctx.author)
await self._try_api_update(str(ctx.author.id), str(ctx.author), credits)
await ctx.send("You have been opted out of the global leaderboard. Your credits will no longer be visible.") await ctx.send("You have been opted out of the global leaderboard. Your credits will no longer be visible.")
@globalboard.command(name="optin") @globalboard.command(name="optin")
@ -306,6 +371,11 @@ class Leaderboard(commands.Cog):
return return
await user_settings.opted_out.set(False) await user_settings.opted_out.set(False)
# Update API with new opt-in status
credits = await self.get_user_credits(ctx.author)
await self._try_api_update(str(ctx.author.id), str(ctx.author), credits)
await ctx.send("You have been opted back into the global leaderboard. Your credits will now be visible.") await ctx.send("You have been opted back into the global leaderboard. Your credits will now be visible.")
@globalboard.command(name="credits") @globalboard.command(name="credits")
@ -353,30 +423,28 @@ class Leaderboard(commands.Cog):
@commands.is_owner() @commands.is_owner()
async def resync_leaderboard(self, ctx: commands.Context): async def resync_leaderboard(self, ctx: commands.Context):
"""Force a resync of all users' credits with the API (if configured).""" """Force a resync of all users' credits with the API (if configured)."""
if not self.session or not self.admin_secret or not self.admin_secret.get("admin_secret"):
await ctx.send("API is not configured.")
return
async with ctx.typing(): async with ctx.typing():
success_count = 0 message = await ctx.send("🔄 Starting global leaderboard resync...")
fail_count = 0
for guild in self.bot.guilds: try:
for member in guild.members: success_count, fail_count = await self._perform_full_sync()
if member.bot:
continue embed = discord.Embed(
title="🔄 Global Leaderboard Resync Complete",
credits = await self.get_user_credits(member) description=(
if await self._try_api_update(str(member.id), str(member), credits): f"Successfully updated: **{success_count}** users\n"
success_count += 1 f"Failed to update: **{fail_count}** users"
else: ),
fail_count += 1 color=await ctx.embed_color(),
timestamp=datetime.now(timezone.utc)
embed = discord.Embed( )
title="🔄 Global Leaderboard Resync Complete", await message.edit(content=None, embed=embed)
description=( except Exception as e:
f"Successfully updated: **{success_count}** users\n" await message.edit(content=f"❌ Error during resync: {str(e)}")
f"Failed to update: **{fail_count}** users"
),
color=await ctx.embed_color()
)
await ctx.send(embed=embed)
@commands.Cog.listener() @commands.Cog.listener()
async def on_bank_update(self, member: discord.Member, before: int, after: int): async def on_bank_update(self, member: discord.Member, before: int, after: int):
@ -419,4 +487,6 @@ class Leaderboard(commands.Cog):
def cog_unload(self): def cog_unload(self):
"""Clean up when cog is unloaded.""" """Clean up when cog is unloaded."""
if self.session: if self.session:
asyncio.create_task(self.session.close()) asyncio.create_task(self.session.close())
if self.auto_sync_task:
self.auto_sync_task.cancel()