diff options
| author | 2021-10-14 21:55:41 +0200 | |
|---|---|---|
| committer | 2021-10-14 19:55:41 +0000 | |
| commit | f18e9c3dda721b9bbbba884e31b34e4ae2831ffc (patch) | |
| tree | bc8720a4a00d7c54d551bdf6172bea24fc3e10cf /bot/exts | |
| parent | Merge pull request #908 from onerandomusername/patch-2 (diff) | |
Add support to query AoC results in respect of days and stars (#857)
* Add support to query AoC results in respect of days and stars
From now on the AoC leaderboard command accepts a total of 2 optional arguments a day and star string (eg.: 1-2, for the second star of the first day) and a number of results they would like to see, with a total maximum of 15.
This commit also introduces a few minor fixes in the AoC helper.
* Improve overall code consitency in the AoC event Cog and helpers
* Improve indenting and code consistency in the AoC cog
* Improve code transparency in the AoC helpers
* Patch various inconsistencies in the AoC cog and helpers
* Migrate AoC Day and Star statistics filtering to Dropdowns
From now on when the AoC leadearboard command is used with the DayAndStar argument(bool)
the bot will send a View with two dropdowns and a button to Fetch the data based on the value
of the Dropdowns.
* Improve code and comment consistency in the AoC views and helpers
* Patch logic errors, improve consistency in the AoC cog and view.
* Add support to delete view from the message after timeout in the AoC cog
* Move the day_and_star logic out of the typing context manager in the AoC cog
* Revert season-locker in the AoC cog
* Improve overall code transparency and indenting in the AoC cog and views
* Remove unnecessary returns in the AoC cog and view
Diffstat (limited to 'bot/exts')
| -rw-r--r-- | bot/exts/events/advent_of_code/_cog.py | 45 | ||||
| -rw-r--r-- | bot/exts/events/advent_of_code/_helpers.py | 9 | ||||
| -rw-r--r-- | bot/exts/events/advent_of_code/views/dayandstarview.py | 71 | 
3 files changed, 118 insertions, 7 deletions
diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index ca60e517..7dd967ec 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -2,6 +2,7 @@ import json  import logging  from datetime import datetime, timedelta  from pathlib import Path +from typing import Optional  import arrow  import discord @@ -12,6 +13,7 @@ from bot.constants import (      AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS,  )  from bot.exts.events.advent_of_code import _helpers +from bot.exts.events.advent_of_code.views.dayandstarview import AoCDropdownView  from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role  from bot.utils.extensions import invoke_help_command @@ -150,7 +152,7 @@ class AdventOfCode(commands.Cog):          else:              try:                  join_code = await _helpers.get_public_join_code(author) -            except _helpers.FetchingLeaderboardFailed: +            except _helpers.FetchingLeaderboardFailedError:                  await ctx.send(":x: Failed to get join code! Notified maintainers.")                  return @@ -185,14 +187,29 @@ class AdventOfCode(commands.Cog):          brief="Get a snapshot of the PyDis private AoC leaderboard",      )      @whitelist_override(channels=AOC_WHITELIST_RESTRICTED) -    async def aoc_leaderboard(self, ctx: commands.Context) -> None: -        """Get the current top scorers of the Python Discord Leaderboard.""" +    async def aoc_leaderboard( +            self, +            ctx: commands.Context, +            day_and_star: Optional[bool] = False, +            maximum_scorers: 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: +            raise commands.BadArgument( +                f"The maximum number of results you can query is {AocConfig.max_day_and_star_results}" +            )          async with ctx.typing():              try:                  leaderboard = await _helpers.fetch_leaderboard() -            except _helpers.FetchingLeaderboardFailed: +            except _helpers.FetchingLeaderboardFailedError:                  await ctx.send(":x: Unable to fetch leaderboard!")                  return +        if not day_and_star:              number_of_participants = leaderboard["number_of_participants"] @@ -203,6 +220,22 @@ class AdventOfCode(commands.Cog):              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, +            original_author=ctx.author +        ) +        message = await ctx.send( +            content="Please select a day and a star to filter by!", +            view=view +        ) +        await view.wait() +        await message.edit(view=None)      @in_month(Month.DECEMBER)      @adventofcode_group.command( @@ -231,7 +264,7 @@ class AdventOfCode(commands.Cog):          """Send an embed with daily completion statistics for the Python Discord leaderboard."""          try:              leaderboard = await _helpers.fetch_leaderboard() -        except _helpers.FetchingLeaderboardFailed: +        except _helpers.FetchingLeaderboardFailedError:              await ctx.send(":x: Can't fetch leaderboard for stats right now!")              return @@ -267,7 +300,7 @@ class AdventOfCode(commands.Cog):          async with ctx.typing():              try:                  await _helpers.fetch_leaderboard(invalidate_cache=True) -            except _helpers.FetchingLeaderboardFailed: +            except _helpers.FetchingLeaderboardFailedError:                  await ctx.send(":x: Something went wrong while trying to refresh the cache!")              else:                  await ctx.send("\N{OK Hand Sign} Refreshed leaderboard cache!") diff --git a/bot/exts/events/advent_of_code/_helpers.py b/bot/exts/events/advent_of_code/_helpers.py index 5fedb60f..af64bc81 100644 --- a/bot/exts/events/advent_of_code/_helpers.py +++ b/bot/exts/events/advent_of_code/_helpers.py @@ -105,6 +105,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:      # The data we get from the AoC website is structured by member, not by day/star,      # which means we need to iterate over the members to transpose the data to a per      # star view. We need that per star view to compute rank scores per star. +    per_day_star_stats = collections.defaultdict(list)      for member in raw_leaderboard_data.values():          name = member["name"] if member["name"] else f"Anonymous #{member['id']}"          member_id = member["id"] @@ -122,6 +123,11 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:                  star_results[(day, star)].append(                      StarResult(member_id=member_id, completion_time=completion_time)                  ) +                per_day_star_stats[f"{day}-{star}"].append( +                    {'completion_time': int(data["get_star_ts"]), 'member_name': name} +                ) +    for key in per_day_star_stats: +        per_day_star_stats[key] = sorted(per_day_star_stats[key], key=operator.itemgetter('completion_time'))      # Now that we have a transposed dataset that holds the completion time of all      # participants per star, we can compute the rank-based scores each participant @@ -151,7 +157,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:          # this data to JSON in order to cache it in Redis.          daily_stats[day] = {"star_one": star_one, "star_two": star_two} -    return {"daily_stats": daily_stats, "leaderboard": sorted_leaderboard} +    return {"daily_stats": daily_stats, "leaderboard": sorted_leaderboard, 'per_day_and_star': per_day_star_stats}  def _format_leaderboard(leaderboard: dict[str, dict]) -> str: @@ -289,6 +295,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict:              "leaderboard_fetched_at": leaderboard_fetched_at,              "number_of_participants": number_of_participants,              "daily_stats": json.dumps(parsed_leaderboard_data["daily_stats"]), +            "leaderboard_per_day_and_star": json.dumps(parsed_leaderboard_data["per_day_and_star"])          }          # Store the new values in Redis diff --git a/bot/exts/events/advent_of_code/views/dayandstarview.py b/bot/exts/events/advent_of_code/views/dayandstarview.py new file mode 100644 index 00000000..243db32e --- /dev/null +++ b/bot/exts/events/advent_of_code/views/dayandstarview.py @@ -0,0 +1,71 @@ +from datetime import datetime + +import discord + +AOC_DAY_AND_STAR_TEMPLATE = "{rank: >4} | {name:25.25} | {completion_time: >10}" + + +class AoCDropdownView(discord.ui.View): +    """Interactive view to filter AoC stats by Day and Star.""" + +    def __init__(self, original_author: discord.Member, day_and_star_data: dict[str: dict], maximum_scorers: int): +        super().__init__() +        self.day = 0 +        self.star = 0 +        self.data = day_and_star_data +        self.maximum_scorers = maximum_scorers +        self.original_author = original_author + +    def generate_output(self) -> str: +        """Generates a formatted codeblock with AoC statistics based on the currently selected day and star.""" +        header = AOC_DAY_AND_STAR_TEMPLATE.format( +            rank="Rank", +            name="Name", completion_time="Completion time (UTC)" +        ) +        lines = [f"{header}\n{'-' * (len(header) + 2)}"] + +        for rank, scorer in enumerate(self.data[f"{self.day}-{self.star}"][:self.maximum_scorers]): +            time_data = datetime.fromtimestamp(scorer['completion_time']).strftime("%I:%M:%S %p") +            lines.append(AOC_DAY_AND_STAR_TEMPLATE.format( +                datastamp="", +                rank=rank + 1, +                name=scorer['member_name'], +                completion_time=time_data) +            ) +        joined_lines = "\n".join(lines) +        return f"Statistics for Day: {self.day}, Star: {self.star}.\n ```\n{joined_lines}\n```" + +    async def interaction_check(self, interaction: discord.Interaction) -> bool: +        """Global check to ensure that the interacting user is the user who invoked the command originally.""" +        return interaction.user == self.original_author + +    @discord.ui.select( +        placeholder="Day", +        options=[discord.SelectOption(label=str(i)) for i in range(1, 26)], +        custom_id="day_select" +    ) +    async def day_select(self, select: discord.ui.Select, interaction: discord.Interaction) -> None: +        """Dropdown to choose a Day of the AoC.""" +        self.day = select.values[0] + +    @discord.ui.select( +        placeholder="Star", +        options=[discord.SelectOption(label=str(i)) for i in range(1, 3)], +        custom_id="star_select" +    ) +    async def star_select(self, select: discord.ui.Select, interaction: discord.Interaction) -> None: +        """Dropdown to choose either the first or the second star.""" +        self.star = select.values[0] + +    @discord.ui.button(label="Fetch", style=discord.ButtonStyle.blurple) +    async def fetch(self, button: discord.ui.Button, interaction: discord.Interaction) -> None: +        """Button that fetches the statistics based on the dropdown values.""" +        if self.day == 0 or self.star == 0: +            await interaction.response.send_message( +                "You have to select a value from both of the dropdowns!", +                ephemeral=True +            ) +        else: +            await interaction.response.edit_message(content=self.generate_output()) +            self.day = 0 +            self.star = 0  |