diff options
| -rw-r--r-- | bot/exts/events/advent_of_code/_cog.py | 73 | ||||
| -rw-r--r-- | bot/exts/events/advent_of_code/_helpers.py | 60 | 
2 files changed, 101 insertions, 32 deletions
diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index 2c1f4541..cd41e9ce 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -180,24 +180,18 @@ class AdventOfCode(commands.Cog):      @in_month(Month.DECEMBER)      @adventofcode_group.command( -        name="leaderboard", -        aliases=("board", "lb"), -        brief="Get a snapshot of the PyDis private AoC leaderboard", +        name="dayandstar", +        aliases=("daynstar", "daystar"), +        brief="Get a view that lets you filter the leaderboard by day and star",      )      @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) -    async def aoc_leaderboard( +    async def aoc_day_and_star_leaderboard(              self,              ctx: commands.Context, -            day_and_star: Optional[bool] = False, -            maximum_scorers: Optional[int] = 10 +            maximum_scorers_day_and_star: Optional[int] = 10      ) -> None: -        """ -        Get the current top scorers of the Python Discord Leaderboard. - -        Additionally, you can provide an argument `day_and_star` (Boolean) to have the bot send a View -        that will let you filter by day and star. -        """ -        if maximum_scorers > AocConfig.max_day_and_star_results or maximum_scorers <= 0: +        """Have the bot send a View that will let you filter the leaderboard by day and star.""" +        if maximum_scorers_day_and_star > AocConfig.max_day_and_star_results or maximum_scorers_day_and_star <= 0:              raise commands.BadArgument(                  f"The maximum number of results you can query is {AocConfig.max_day_and_star_results}"              ) @@ -207,25 +201,12 @@ class AdventOfCode(commands.Cog):              except _helpers.FetchingLeaderboardFailedError:                  await ctx.send(":x: Unable to fetch leaderboard!")                  return -        if not day_and_star: - -            number_of_participants = leaderboard["number_of_participants"] - -            top_count = min(AocConfig.leaderboard_displayed_members, number_of_participants) -            header = f"Here's our current top {top_count}! {Emojis.christmas_tree * 3}" - -            table = f"```\n{leaderboard['top_leaderboard']}\n```" -            info_embed = _helpers.get_summary_embed(leaderboard) - -            await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) -            return -          # This is a dictionary that contains solvers in respect of day, and star.          # e.g. 1-1 means the solvers of the first star of the first day and their completion time          per_day_and_star = json.loads(leaderboard['leaderboard_per_day_and_star'])          view = AoCDropdownView(              day_and_star_data=per_day_and_star, -            maximum_scorers=maximum_scorers, +            maximum_scorers=maximum_scorers_day_and_star,              original_author=ctx.author          )          message = await ctx.send( @@ -237,6 +218,44 @@ class AdventOfCode(commands.Cog):      @in_month(Month.DECEMBER)      @adventofcode_group.command( +        name="leaderboard", +        aliases=("board", "lb"), +        brief="Get a snapshot of the PyDis private AoC leaderboard", +    ) +    @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) +    async def aoc_leaderboard( +            self, +            ctx: commands.Context, +            self_placement_name: Optional[str] = None, +    ) -> None: +        """ +        Get the current top scorers of the Python Discord Leaderboard. + +        Additionally you can specify a `self_placement_name` +        that will append the specified profile's personal stats to the top of the leaderboard +        """ +        async with ctx.typing(): +            try: +                leaderboard = await _helpers.fetch_leaderboard(self_placement_name=self_placement_name) +            except _helpers.FetchingLeaderboardFailedError: +                await ctx.send(":x: Unable to fetch leaderboard!") +                return + +        number_of_participants = leaderboard["number_of_participants"] + +        top_count = min(AocConfig.leaderboard_displayed_members, number_of_participants) +        self_placement_header = "(and your personal stats compared to the top 10)" if self_placement_name else "" +        header = f"Here's our current top {top_count}{self_placement_header}! {Emojis.christmas_tree * 3}" +        table = "```\n" \ +                f"{leaderboard['placement_leaderboard'] if self_placement_name else leaderboard['top_leaderboard']}" \ +                "\n```" +        info_embed = _helpers.get_summary_embed(leaderboard) + +        await ctx.send(content=f"{header}\n\n{table}", embed=info_embed) +        return + +    @in_month(Month.DECEMBER) +    @adventofcode_group.command(          name="global",          aliases=("globalboard", "gb"),          brief="Get a link to the global leaderboard", diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index af64bc81..35258544 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -10,6 +10,7 @@ from typing import Any, Optional  import aiohttp  import arrow  import discord +from discord.ext import commands  from bot.bot import Bot  from bot.constants import AdventOfCode, Channels, Colours @@ -70,6 +71,33 @@ class FetchingLeaderboardFailedError(Exception):      """Raised when one or more leaderboards could not be fetched at all.""" +def _format_leaderboard_line(rank: int, data: dict[str, Any], *, is_author: bool) -> str: +    """ +    Build a string representing a line of the leaderboard. + +    Parameters: +        rank: +            Rank in the leaderboard of this entry. + +        data: +            Mapping with entry information. + +    Keyword arguments: +        is_author: +            Whether to address the name displayed in the returned line +            personally. + +    Returns: +        A formatted line for the leaderboard. +    """ +    return AOC_TABLE_TEMPLATE.format( +        rank=rank, +        name=data['name'] if not is_author else f"(You) {data['name']}", +        score=str(data['score']), +        stars=f"({data['star_1']}, {data['star_2']})" +    ) + +  def leaderboard_sorting_function(entry: tuple[str, dict]) -> tuple[int, int]:      """      Provide a sorting value for our leaderboard. @@ -160,10 +188,23 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:      return {"daily_stats": daily_stats, "leaderboard": sorted_leaderboard, 'per_day_and_star': per_day_star_stats} -def _format_leaderboard(leaderboard: dict[str, dict]) -> str: +def _format_leaderboard(leaderboard: dict[str, dict], self_placement_name: str = None) -> str:      """Format the leaderboard using the AOC_TABLE_TEMPLATE."""      leaderboard_lines = [HEADER] +    self_placement_exists = False      for rank, data in enumerate(leaderboard.values(), start=1): +        if self_placement_name and data["name"].lower() == self_placement_name.lower(): +            leaderboard_lines.insert( +                1, +                AOC_TABLE_TEMPLATE.format( +                    rank=rank, +                    name=f"(You) {data['name']}", +                    score=str(data["score"]), +                    stars=f"({data['star_1']}, {data['star_2']})" +                ) +            ) +            self_placement_exists = True +            continue          leaderboard_lines.append(              AOC_TABLE_TEMPLATE.format(                  rank=rank, @@ -172,7 +213,10 @@ def _format_leaderboard(leaderboard: dict[str, dict]) -> str:                  stars=f"({data['star_1']}, {data['star_2']})"              )          ) - +    if self_placement_name and not self_placement_exists: +        raise commands.BadArgument( +            "Sorry, your profile does not exist in this leaderboard." +        )      return "\n".join(leaderboard_lines) @@ -260,7 +304,7 @@ def _get_top_leaderboard(full_leaderboard: str) -> str:  @_caches.leaderboard_cache.atomic_transaction -async def fetch_leaderboard(invalidate_cache: bool = False) -> dict: +async def fetch_leaderboard(invalidate_cache: bool = False, self_placement_name: str = None) -> dict:      """      Get the current Python Discord combined leaderboard. @@ -270,7 +314,6 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict:      miss, this function is locked to one call at a time using a decorator.      """      cached_leaderboard = await _caches.leaderboard_cache.to_dict() -      # Check if the cached leaderboard contains everything we expect it to. If it      # does not, this probably means the cache has not been created yet or has      # expired in Redis. This check also accounts for a malformed cache. @@ -289,6 +332,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict:          leaderboard_fetched_at = datetime.datetime.utcnow().isoformat()          cached_leaderboard = { +            "placement_leaderboard": json.dumps(raw_leaderboard_data),              "full_leaderboard": formatted_leaderboard,              "top_leaderboard": _get_top_leaderboard(formatted_leaderboard),              "full_leaderboard_url": full_leaderboard_url, @@ -307,7 +351,13 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict:                  _caches.leaderboard_cache.namespace,                  AdventOfCode.leaderboard_cache_expiry_seconds              ) - +    if self_placement_name: +        formatted_placement_leaderboard = _parse_raw_leaderboard_data( +            json.loads(cached_leaderboard["placement_leaderboard"]) +        )["leaderboard"] +        cached_leaderboard["placement_leaderboard"] = _get_top_leaderboard( +            _format_leaderboard(formatted_placement_leaderboard, self_placement_name=self_placement_name) +        )      return cached_leaderboard  |