diff options
| author | 2021-10-26 07:38:22 -0400 | |
|---|---|---|
| committer | 2021-10-26 07:38:22 -0400 | |
| commit | 93fa57ae4ad6892f660d841eb1f28b9f1bf9b2c2 (patch) | |
| tree | 48c1590bb846514be2c1897e547bd3ad5156fc3f /bot/exts/events | |
| parent | Merge branch 'color-677' of https://github.com/brad90four/sir-lancebot into c... (diff) | |
| parent | chore: code cleanup (diff) | |
Merge branch 'color-677' of https://github.com/brad90four/sir-lancebot into color-677
Diffstat (limited to 'bot/exts/events')
| -rw-r--r-- | bot/exts/events/advent_of_code/_cog.py | 49 | ||||
| -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 | ||||
| -rw-r--r-- | bot/exts/events/hacktoberfest/hacktober-issue-finder.py | 3 | 
4 files changed, 121 insertions, 11 deletions
| diff --git a/bot/exts/events/advent_of_code/_cog.py b/bot/exts/events/advent_of_code/_cog.py index ca60e517..2c1f4541 100644 --- a/bot/exts/events/advent_of_code/_cog.py +++ b/bot/exts/events/advent_of_code/_cog.py @@ -2,16 +2,16 @@ import json  import logging  from datetime import datetime, timedelta  from pathlib import Path +from typing import Optional  import arrow  import discord  from discord.ext import commands  from bot.bot import Bot -from bot.constants import ( -    AdventOfCode as AocConfig, Channels, Colours, Emojis, Month, Roles, WHITELISTED_CHANNELS, -) +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 +150,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 +185,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 +218,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 +262,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 +298,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 diff --git a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py index 088e7e43..1774564b 100644 --- a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py +++ b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py @@ -100,7 +100,8 @@ class HacktoberIssues(commands.Cog):          """Format the issue data into a embed."""          title = issue["title"]          issue_url = issue["url"].replace("api.", "").replace("/repos/", "/") -        body = issue["body"] +        # issues can have empty bodies, which in that case GitHub doesn't include the key in the API response +        body = issue.get("body", "")          labels = [label["name"] for label in issue["labels"]]          embed = discord.Embed(title=title) | 
