Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions bot/exts/fun/leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from bot.bot import Bot
from bot.constants import Colours, MODERATION_ROLES
from bot.utils.decorators import with_role
from bot.utils.leaderboard import get_daily_leaderboard, get_leaderboard, get_user_points, get_user_rank
from bot.utils.leaderboard import POINTS_CACHE, get_daily_leaderboard, get_leaderboard, get_user_points, get_user_rank
from bot.utils.pagination import LinePaginator

DUCK_COIN_THUMBNAIL = (
Expand Down Expand Up @@ -58,10 +58,12 @@ async def interaction_check(self, interaction: Interaction) -> bool:
@ui.button(label="Confirm", style=ButtonStyle.danger)
async def confirm(self, interaction: Interaction, _button: ui.Button) -> None:
"""Clear the leaderboard on confirmation."""
from bot.utils.leaderboard import _get_points_cache
if POINTS_CACHE is None:
await interaction.response.send_message("Leaderboard cache is not initialized.")
self.stop()
return

points_cache = await _get_points_cache()
await points_cache.clear()
await POINTS_CACHE.clear()
await interaction.response.send_message("Leaderboard has been cleared.")
self.stop()

Expand All @@ -80,6 +82,11 @@ class Leaderboard(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot

async def cog_load(self) -> None:
"""Register the global cache when the cog loads."""
from bot.utils import leaderboard
leaderboard.POINTS_CACHE = self.points_cache

@commands.group(name="leaderboard", aliases=("lb", "points"), invoke_without_command=True)
async def leaderboard_command(self, ctx: commands.Context) -> None:
"""Show the global game points leaderboard."""
Expand Down
2 changes: 1 addition & 1 deletion bot/exts/holidays/easter/egghead_quiz.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ async def eggquiz(self, ctx: commands.Context) -> None:
# with the correct answer, so stop looping over reactions.
break

winners = [u for u in users if not u.bot]
winners = tuple(u for u in users if not u.bot)

points_earned = {}
for u in winners:
Expand Down
35 changes: 12 additions & 23 deletions bot/utils/leaderboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,15 @@
# Prefix for daily cap keys stored directly in Redis (not via RedisCache).
_DAILY_KEY_PREFIX = "leaderboard:daily"

#Global points cache, set by the Leaderboard cog on load.
POINTS_CACHE: RedisCache | None = None


def _daily_key(user_id: int, game_name: str) -> str:
"""Build a namespaced Redis key for daily point tracking."""
return f"{_DAILY_KEY_PREFIX}:{user_id}:{game_name}"


async def _get_points_cache() -> RedisCache:
"""Get the persistent points cache from the Leaderboard cog."""
from bot.exts.fun.leaderboard import Leaderboard
return Leaderboard.points_cache


async def add_points(bot: Bot, user_id: int, points: int, game_name: str) -> tuple[int, int]:
"""
Add points to a user's global leaderboard score.
Expand Down Expand Up @@ -68,13 +65,9 @@ async def add_points(bot: Bot, user_id: int, points: int, game_name: str) -> tup
await redis.set(daily_key, earned_today + points_earned, ex=ttl)

# update persistent global total
points_cache = await _get_points_cache()
if await points_cache.contains(user_id):
await points_cache.increment(user_id, points_earned)
else:
await points_cache.set(user_id, points_earned)
await POINTS_CACHE.increment(user_id, points_earned)

new_total = int(await points_cache.get(user_id))
new_total = int(await POINTS_CACHE.get(user_id))
return (new_total, points_earned)


Expand All @@ -85,18 +78,16 @@ async def remove_points(bot: Bot, user_id: int, points: int) -> int:
Score will not go below 0. Returns the user's new total score,
or 0 if the cog is not loaded.
"""
if points <= 0 or bot.get_cog("Leaderboard") is None:
if points <= 0 or bot.get_cog("Leaderboard") is None or POINTS_CACHE is None:
return await get_user_points(bot, user_id)

points_cache = await _get_points_cache()

current = await points_cache.get(user_id)
current = await POINTS_CACHE.get(user_id)
if not current:
return 0

current = int(current)
to_remove = min(points, current)
await points_cache.decrement(user_id, to_remove)
await POINTS_CACHE.decrement(user_id, to_remove)

return current - to_remove

Expand All @@ -107,11 +98,10 @@ async def get_leaderboard(bot: Bot) -> list[tuple[int, int]]:

Returns a list of (user_id, score) tuples sorted by score descending.
"""
if bot.get_cog("Leaderboard") is None:
if bot.get_cog("Leaderboard") is None or POINTS_CACHE is None:
return []

points_cache = await _get_points_cache()
records = await points_cache.items()
records = await POINTS_CACHE.items()

return sorted(
((int(user_id), int(score)) for user_id, score in records if int(score) > 0),
Expand Down Expand Up @@ -176,9 +166,8 @@ async def get_user_rank(

async def get_user_points(bot: Bot, user_id: int) -> int:
"""Get a specific user's total points."""
if bot.get_cog("Leaderboard") is None:
if bot.get_cog("Leaderboard") is None or POINTS_CACHE is None:
return 0

points_cache = await _get_points_cache()
score = await points_cache.get(user_id)
score = await POINTS_CACHE.get(user_id)
return int(score) if score else 0