diff options
Diffstat (limited to 'bot')
| -rw-r--r-- | bot/exts/christmas/advent_of_code/_cog.py | 27 | ||||
| -rw-r--r-- | bot/exts/christmas/advent_of_code/_helpers.py | 65 | 
2 files changed, 75 insertions, 17 deletions
| diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py index 2a1a776b..0bcd9f42 100644 --- a/bot/exts/christmas/advent_of_code/_cog.py +++ b/bot/exts/christmas/advent_of_code/_cog.py @@ -221,7 +221,11 @@ class AdventOfCode(commands.Cog):          if AocConfig.staff_leaderboard_id and any(r.id == Roles.helpers for r in author.roles):              join_code = AocConfig.leaderboards[AocConfig.staff_leaderboard_id].join_code          else: -            join_code = await _helpers.get_public_join_code(author) +            try: +                join_code = await _helpers.get_public_join_code(author) +            except _helpers.FetchingLeaderboardFailed: +                await ctx.send(":x: Failed to get join code! Notified maintainers.") +                return          if not join_code:              log.error(f"Failed to get a join code for user {author} ({author.id})") @@ -256,7 +260,12 @@ class AdventOfCode(commands.Cog):      async def aoc_leaderboard(self, ctx: commands.Context) -> None:          """Get the current top scorers of the Python Discord Leaderboard."""          async with ctx.typing(): -            leaderboard = await _helpers.fetch_leaderboard() +            try: +                leaderboard = await _helpers.fetch_leaderboard() +            except _helpers.FetchingLeaderboardFailed: +                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) @@ -291,7 +300,11 @@ class AdventOfCode(commands.Cog):      @override_in_channel(AOC_WHITELIST)      async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None:          """Send an embed with daily completion statistics for the Python Discord leaderboard.""" -        leaderboard = await _helpers.fetch_leaderboard() +        try: +            leaderboard = await _helpers.fetch_leaderboard() +        except _helpers.FetchingLeaderboardFailed: +            await ctx.send(":x: Can't fetch leaderboard for stats right now!") +            return          # The daily stats are serialized as JSON as they have to be cached in Redis          daily_stats = json.loads(leaderboard["daily_stats"]) @@ -323,8 +336,12 @@ class AdventOfCode(commands.Cog):          many requests to the Advent of Code server.          """          async with ctx.typing(): -            await _helpers.fetch_leaderboard(invalidate_cache=True) -            await ctx.send("\N{OK Hand Sign} Refreshed leaderboard cache!") +            try: +                await _helpers.fetch_leaderboard(invalidate_cache=True) +            except _helpers.FetchingLeaderboardFailed: +                await ctx.send(":x: Something went wrong while trying to refresh the cache!") +            else: +                await ctx.send("\N{OK Hand Sign} Refreshed leaderboard cache!")      def cog_unload(self) -> None:          """Cancel season-related tasks on cog unload.""" diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py index e7eeedb2..d883c09f 100644 --- a/bot/exts/christmas/advent_of_code/_helpers.py +++ b/bot/exts/christmas/advent_of_code/_helpers.py @@ -53,6 +53,18 @@ EST = pytz.timezone("EST")  StarResult = collections.namedtuple("StarResult", "member_id completion_time") +class UnexpectedRedirect(aiohttp.ClientError): +    """Raised when an unexpected redirect was detected.""" + + +class UnexpectedResponseStatus(aiohttp.ClientError): +    """Raised when an unexpected redirect was detected.""" + + +class FetchingLeaderboardFailed(Exception): +    """Raised when one or more leaderboards could not be fetched at all.""" + +  def leaderboard_sorting_function(entry: typing.Tuple[str, dict]) -> typing.Tuple[int, int]:      """      Provide a sorting value for our leaderboard. @@ -153,6 +165,23 @@ def _format_leaderboard(leaderboard: typing.Dict[str, dict]) -> str:      return "\n".join(leaderboard_lines) +async def _leaderboard_request(url: str, board: int, cookies: dict) -> typing.Optional[dict]: +    """Make a leaderboard request using the specified session cookie.""" +    async with aiohttp.request("GET", url, headers=AOC_REQUEST_HEADER, cookies=cookies) as resp: +        # The Advent of Code website redirects silently with a 200 response if a +        # session cookie has expired, is invalid, or was not provided. +        if str(resp.url) != url: +            log.error(f"Fetching leaderboard `{board}` failed! Check the session cookie.") +            raise UnexpectedRedirect(f"redirected unexpectedly to {resp.url} for board `{board}`") + +        # Every status other than `200` is unexpected, not only 400+ +        if not resp.status == 200: +            log.error(f"Unexpected response `{resp.status}` while fetching leaderboard `{board}`") +            raise UnexpectedResponseStatus(f"status `{resp.status}`") + +        return await resp.json() + +  async def _fetch_leaderboard_data() -> typing.Dict[str, typing.Any]:      """Fetch data for all leaderboards and return a pooled result."""      year = AdventOfCode.year @@ -165,22 +194,34 @@ async def _fetch_leaderboard_data() -> typing.Dict[str, typing.Any]:      participants = {}      for leaderboard in AdventOfCode.leaderboards.values():          leaderboard_url = AOC_API_URL.format(year=year, leaderboard_id=leaderboard.id) -        cookies = {"session": leaderboard.session} -        # We don't need to create a session if we're going to throw it away after each request -        async with aiohttp.request( -            "GET", leaderboard_url, headers=AOC_REQUEST_HEADER, cookies=cookies -        ) as resp: -            if resp.status == 200: -                raw_data = await resp.json() - -                # Get the participants and store their current count +        # Two attempts, one with the original session cookie and one with the fallback session +        for attempt in range(1, 3): +            log.info(f"Attempting to fetch leaderboard `{leaderboard.id}` ({attempt}/2)") +            cookies = {"session": leaderboard.session} +            try: +                raw_data = await _leaderboard_request(leaderboard_url, leaderboard.id, cookies) +            except UnexpectedRedirect: +                if cookies["session"] == AdventOfCode.fallback_session: +                    log.error("It seems like the fallback cookie has expired!") +                    raise FetchingLeaderboardFailed from None + +                # If we're here, it means that the original session did not +                # work. Let's fall back to the fallback session. +                leaderboard.use_fallback_session = True +                continue +            except aiohttp.ClientError: +                # Don't retry, something unexpected is wrong and it may not be the session. +                raise FetchingLeaderboardFailed from None +            else: +                # Get the participants and store their current count.                  board_participants = raw_data["members"]                  await _caches.leaderboard_counts.set(leaderboard.id, len(board_participants))                  participants.update(board_participants) -            else: -                log.warning(f"Fetching data failed for leaderboard `{leaderboard.id}`") -                resp.raise_for_status() +                break +        else: +            log.error(f"reached 'unreachable' state while fetching board `{leaderboard.id}`.") +            raise FetchingLeaderboardFailed      log.info(f"Fetched leaderboard information for {len(participants)} participants")      return participants | 
