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
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:
parent
bc602b73bf
commit
02d67baeef
1 changed files with 134 additions and 64 deletions
|
@ -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()
|
Loading…
Add table
Reference in a new issue