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.admin_secret = None
# Add task for auto-sync
self.auto_sync_task = None
default_guild = {
"min_message_length": 5,
"cooldown": 60,
@ -95,7 +98,7 @@ class Leaderboard(commands.Cog):
self.config.register_user(**default_user)
async def initialize(self):
"""Initialize optional API connection."""
"""Initialize optional API connection and start auto-sync task."""
try:
self.admin_secret = await self.bot.get_shared_api_tokens("ruby_api")
if self.admin_secret.get("admin_secret"):
@ -107,9 +110,56 @@ class Leaderboard(commands.Cog):
) as resp:
if resp.status != 200:
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:
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):
"""Initialize the cog when it's loaded."""
await self.initialize()
@ -118,54 +168,60 @@ class Leaderboard(commands.Cog):
"""Get a user's total credits from all servers."""
total_credits = 0
# Get credits from all servers the user is in
for guild in self.bot.guilds:
if user in guild.members: # Only check servers where the user is a member
try:
credits = await bank.get_balance(user)
total_credits += credits
except Exception as e:
log.error(f"Error getting bank balance for {user} in {guild}: {e}")
continue
try:
# Get credits from the user's current server first
total_credits = await bank.get_balance(user)
# Then add credits from all other servers where they are a member
for guild in self.bot.guilds:
if guild != user.guild and user in guild.members: # Skip current guild since we already got those credits
try:
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
async def get_all_balances(self) -> List[dict]:
"""Get all users' credit balances across all servers."""
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 member in guild.members:
if member.bot:
continue
# 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
if not member.bot:
all_members.add(member)
# 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(
[user for user in all_users.values() if user["points"] >= min_credits],
all_users.values(),
key=lambda x: x["points"],
reverse=True
)
@ -180,6 +236,9 @@ class Leaderboard(commands.Cog):
try:
if credits < 0:
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(
f"{self.api_base_url}/leaderboard",
@ -190,13 +249,14 @@ class Leaderboard(commands.Cog):
json={
"userId": str(user_id),
"username": str(username),
"points": credits
"points": credits,
"optedOut": opted_out
}
) as resp:
if resp.status == 200:
data = await resp.json()
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
else:
log.error(f"API update failed: {data.get('error', 'Unknown error')}")
@ -293,6 +353,11 @@ class Leaderboard(commands.Cog):
return
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.")
@globalboard.command(name="optin")
@ -306,6 +371,11 @@ class Leaderboard(commands.Cog):
return
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.")
@globalboard.command(name="credits")
@ -353,30 +423,28 @@ class Leaderboard(commands.Cog):
@commands.is_owner()
async def resync_leaderboard(self, ctx: commands.Context):
"""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():
success_count = 0
fail_count = 0
message = await ctx.send("🔄 Starting global leaderboard resync...")
for guild in self.bot.guilds:
for member in guild.members:
if member.bot:
continue
credits = await self.get_user_credits(member)
if await self._try_api_update(str(member.id), str(member), credits):
success_count += 1
else:
fail_count += 1
embed = discord.Embed(
title="🔄 Global Leaderboard Resync Complete",
description=(
f"Successfully updated: **{success_count}** users\n"
f"Failed to update: **{fail_count}** users"
),
color=await ctx.embed_color()
)
await ctx.send(embed=embed)
try:
success_count, fail_count = await self._perform_full_sync()
embed = discord.Embed(
title="🔄 Global Leaderboard Resync Complete",
description=(
f"Successfully updated: **{success_count}** users\n"
f"Failed to update: **{fail_count}** users"
),
color=await ctx.embed_color(),
timestamp=datetime.now(timezone.utc)
)
await message.edit(content=None, embed=embed)
except Exception as e:
await message.edit(content=f"❌ Error during resync: {str(e)}")
@commands.Cog.listener()
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):
"""Clean up when cog is unloaded."""
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()