Refactor Leaderboard cog to improve session management and error handling. Initialize aiohttp session in setup, enhance admin secret validation, and provide clearer error messages for API interactions. Update leaderboard command responses for better user feedback and handling of empty data scenarios.
Some checks are pending
Run pre-commit / Run pre-commit (push) Waiting to run

This commit is contained in:
Valerie 2025-05-26 03:59:48 -04:00
parent 899bfc7bc0
commit 4b17dfedc3

View file

@ -15,7 +15,7 @@ class Leaderboard(commands.Cog):
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
self.config = Config.get_conf(self, identifier=867530999, force_registration=True) self.config = Config.get_conf(self, identifier=867530999, force_registration=True)
self.session = aiohttp.ClientSession() self.session = None # Initialize in setup
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
self.cache_time = 300 # 5 minutes cache self.cache_time = 300 # 5 minutes cache
@ -33,17 +33,29 @@ class Leaderboard(commands.Cog):
self.config.register_guild(**default_guild) self.config.register_guild(**default_guild)
def cog_unload(self): def cog_unload(self):
asyncio.create_task(self.session.close()) if self.session:
asyncio.create_task(self.session.close())
async def initialize(self): async def initialize(self):
"""Load the admin secret from bot config.""" """Load the admin secret from bot config and initialize session."""
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 not self.admin_secret.get("admin_secret"): if not self.admin_secret.get("admin_secret"):
log.error("No admin secret found. Leaderboard functionality will be limited.") log.error("No admin secret found. Please set it using [p]set api ruby_api admin_secret,<your_secret>")
return False
# Initialize aiohttp session
if not self.session:
self.session = aiohttp.ClientSession()
return True
async def _get_leaderboard(self) -> Optional[list]: async def _get_leaderboard(self) -> Optional[list]:
"""Fetch the global leaderboard from the API.""" """Fetch the global leaderboard from the API."""
if not self.admin_secret: if not self.admin_secret or not self.admin_secret.get("admin_secret"):
log.error("Admin secret not configured")
return None
if not self.session:
log.error("Session not initialized")
return None return None
try: try:
@ -59,19 +71,33 @@ class Leaderboard(commands.Cog):
) as resp: ) as resp:
if resp.status == 200: if resp.status == 200:
data = await resp.json() data = await resp.json()
self._cache["leaderboard"] = data.get("leaderboard", []) if not isinstance(data, dict) or "leaderboard" not in data:
log.error("Invalid API response format")
return None
self._cache["leaderboard"] = data["leaderboard"]
self._last_update["leaderboard"] = now self._last_update["leaderboard"] = now
return self._cache["leaderboard"] return self._cache["leaderboard"]
else: elif resp.status == 401:
log.error(f"Failed to fetch leaderboard: {resp.status}") log.error("Unauthorized: Invalid admin secret")
return None return None
else:
log.error(f"Failed to fetch leaderboard: Status {resp.status}")
return None
except aiohttp.ClientError as e:
log.error(f"API connection error: {e}")
return None
except Exception as e: except Exception as e:
log.error(f"Error fetching leaderboard: {e}") log.error(f"Unexpected error fetching leaderboard: {e}")
return None return None
async def _update_points(self, user_id: str, username: str, points: int) -> bool: async def _update_points(self, user_id: str, username: str, points: int) -> bool:
"""Update a user's points in the global leaderboard.""" """Update a user's points in the global leaderboard."""
if not self.admin_secret: if not self.admin_secret or not self.admin_secret.get("admin_secret"):
log.error("Admin secret not configured")
return False
if not self.session:
log.error("Session not initialized")
return False return False
try: try:
@ -87,16 +113,29 @@ class Leaderboard(commands.Cog):
"points": points "points": points
} }
) as resp: ) as resp:
return resp.status == 200 if resp.status == 200:
# Clear cache to ensure fresh data on next fetch
self._cache.pop("leaderboard", None)
return True
elif resp.status == 401:
log.error("Unauthorized: Invalid admin secret")
return False
else:
log.error(f"Failed to update points: Status {resp.status}")
return False
except aiohttp.ClientError as e:
log.error(f"API connection error: {e}")
return False
except Exception as e: except Exception as e:
log.error(f"Error updating points: {e}") log.error(f"Unexpected error updating points: {e}")
return False return False
@commands.group(name="globalboard", aliases=["glb"]) @commands.group(name="globalboard", aliases=["glb"])
async def globalboard(self, ctx: commands.Context): async def globalboard(self, ctx: commands.Context):
"""Global leaderboard commands.""" """Global leaderboard commands."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help(ctx.command) # Don't send help here, just show the base command response
await ctx.send("Use `!help globalboard` to see available commands.")
@globalboard.command(name="show") @globalboard.command(name="show")
async def show_leaderboard(self, ctx: commands.Context, page: int = 1): async def show_leaderboard(self, ctx: commands.Context, page: int = 1):
@ -105,12 +144,20 @@ class Leaderboard(commands.Cog):
leaderboard_data = await self._get_leaderboard() leaderboard_data = await self._get_leaderboard()
if not leaderboard_data: if not leaderboard_data:
return await ctx.send("Failed to fetch leaderboard data.") if not self.admin_secret or not self.admin_secret.get("admin_secret"):
return await ctx.send("Leaderboard is not configured. Please contact the bot administrator.")
return await ctx.send("Failed to fetch leaderboard data. Please try again later.")
if not leaderboard_data: # Empty leaderboard
return await ctx.send("The leaderboard is currently empty!")
items_per_page = 10 items_per_page = 10
pages = [leaderboard_data[i:i + items_per_page] pages = [leaderboard_data[i:i + items_per_page]
for i in range(0, len(leaderboard_data), items_per_page)] for i in range(0, len(leaderboard_data), items_per_page)]
if not pages: # No data after pagination
return await ctx.send("The leaderboard is currently empty!")
if not 1 <= page <= len(pages): if not 1 <= page <= len(pages):
return await ctx.send(f"Invalid page number. Please choose between 1 and {len(pages)}.") return await ctx.send(f"Invalid page number. Please choose between 1 and {len(pages)}.")
@ -121,10 +168,13 @@ class Leaderboard(commands.Cog):
start_pos = (page - 1) * items_per_page start_pos = (page - 1) * items_per_page
for i, entry in enumerate(entries, start=start_pos + 1): for i, entry in enumerate(entries, start=start_pos + 1):
username = entry["username"] username = entry.get("username", "Unknown User")
points = entry["points"] points = entry.get("points", 0)
lines.append(f"{i}. {username}: {points:,} points") lines.append(f"{i}. {username}: {points:,} points")
if not lines: # No valid entries
return await ctx.send("No valid leaderboard entries found.")
header = f"🏆 Global Leaderboard (Page {page}/{len(pages)})" header = f"🏆 Global Leaderboard (Page {page}/{len(pages)})"
footer = f"Use {ctx.prefix}globalboard show <page> to view other pages" footer = f"Use {ctx.prefix}globalboard show <page> to view other pages"
@ -138,21 +188,23 @@ class Leaderboard(commands.Cog):
leaderboard_data = await self._get_leaderboard() leaderboard_data = await self._get_leaderboard()
if not leaderboard_data: if not leaderboard_data:
return await ctx.send("Failed to fetch leaderboard data.") if not self.admin_secret or not self.admin_secret.get("admin_secret"):
return await ctx.send("Leaderboard is not configured. Please contact the bot administrator.")
return await ctx.send("Failed to fetch leaderboard data. Please try again later.")
user_data = next( user_data = next(
(entry for entry in leaderboard_data if entry["userId"] == str(member.id)), (entry for entry in leaderboard_data if entry.get("userId") == str(member.id)),
None None
) )
if user_data: if user_data:
rank = next( rank = next(
(i for i, entry in enumerate(leaderboard_data, 1) (i for i, entry in enumerate(leaderboard_data, 1)
if entry["userId"] == str(member.id)), if entry.get("userId") == str(member.id)),
None None
) )
await ctx.send( await ctx.send(
f"🏆 **{member.display_name}** has **{user_data['points']:,}** points " f"🏆 **{member.display_name}** has **{user_data.get('points', 0):,}** points "
f"(Rank: #{rank:,})" f"(Rank: #{rank:,})"
) )
else: else:
@ -189,11 +241,11 @@ class Leaderboard(commands.Cog):
if leaderboard_data: if leaderboard_data:
user_data = next( user_data = next(
(entry for entry in leaderboard_data if entry["userId"] == str(message.author.id)), (entry for entry in leaderboard_data if entry.get("userId") == str(message.author.id)),
None None
) )
if user_data: if user_data:
current_points = user_data["points"] current_points = user_data.get("points", 0)
# Update points # Update points
new_points = current_points + points_per_message new_points = current_points + points_per_message