Skip to content

Commit e41ae8d

Browse files
committed
Remake advent of code cog.
- Remove tree.sync on setup + Add tree.sync on connection to gateway + Ability to load cogs in module format + Slashcommand error are logged as expected
1 parent 248f503 commit e41ae8d

File tree

9 files changed

+615
-448
lines changed

9 files changed

+615
-448
lines changed

Pipfile

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ aiodns = "~=3.0.0"
1717
PyJWT = "~=2.3.0"
1818
matplotlib = "*"
1919
"discord.py" = "*"
20+
pydantic = "*"
2021

2122
[dev-packages]
2223
flake8 = "~=4.0.1"

Pipfile.lock

+446-353
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bot/__main__.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import importlib
23
import inspect
34
import pkgutil
45
import socket
@@ -23,9 +24,12 @@ def on_error(name: str) -> NoReturn:
2324
raise ImportError(name=name)
2425

2526
for module in pkgutil.walk_packages(cogs.__path__, f"{cogs.__name__}.", onerror=on_error):
27+
if any(name.startswith("_") for name in module.name.split(".")):
28+
continue # Ignore modules/packages with a name starting with _
29+
2630
if module.ispkg:
27-
_import = __import__(module.name)
28-
if not inspect.isfunction(getattr(_import, "setup", None)):
31+
imported = importlib.import_module(module.name)
32+
if not inspect.isfunction(getattr(imported, "setup", None)):
2933
continue
3034

3135
yield module.name

bot/bot.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import logging
2+
from typing import Union
23

34
import aiohttp
4-
5+
import discord
56
from discord.ext.commands import Bot, CommandError, Context
67

78
from bot.disable import DisableApi
@@ -34,16 +35,20 @@ def __init__(
3435
self.graphql = GraphQLClient(session=session)
3536

3637
async def setup_hook(self) -> None:
37-
"""Sync the application command tree before bot connects for commands."""
38-
await self.tree.sync()
38+
"""Assign a error handler for Interaction commands."""
39+
self.tree.on_error = self.on_command_error
3940

40-
@staticmethod
41-
async def on_ready() -> None:
42-
"""Runs when the bot is connected."""
41+
async def on_ready(self) -> None:
42+
"""Runs when the bot is connected. Sync Interaction/app_commands when connected to the gateway."""
4343
log.info('Awaiting...')
4444
log.info("Bot Is Ready For Commands")
45+
await self.tree.sync()
4546

46-
async def on_command_error(self, ctx: Context, exception: CommandError) -> None:
47+
async def on_command_error(
48+
self,
49+
ctx: Union[Context, discord.Interaction],
50+
exception: Union[CommandError, discord.app_commands.AppCommandError]
51+
) -> None:
4752
"""Fired when exception happens."""
4853
log.error(
4954
"Exception happened while executing command",

bot/cogs/advent_of_code.py

-84
This file was deleted.

bot/cogs/advent_of_code/__init__.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import logging
2+
3+
from bot.bot import Friendo
4+
from bot.settings import AOC_JOIN_CODE
5+
from ._cog import AdventOfCode
6+
7+
logger = logging.getLogger("advent_of_code")
8+
9+
10+
async def setup(bot: Friendo) -> None:
11+
"""Sets up the AdventOfCode cog."""
12+
if AOC_JOIN_CODE is not None:
13+
await bot.add_cog(AdventOfCode(bot))
14+
else:
15+
logger.warning("Skipping setup for advent of code as a `AOC_JOIN_CODE` wasn't provided")

bot/cogs/advent_of_code/_cog.py

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import logging
2+
from datetime import datetime
3+
from itertools import cycle
4+
from operator import attrgetter
5+
6+
import discord
7+
from discord import app_commands, Embed, Color
8+
from discord.ext import commands
9+
10+
from bot.bot import Friendo
11+
from bot.settings import AOC_JOIN_CODE, AOC_SESSION_COOKIE, AOC_LEADERBOARD_ID
12+
from ._types import Leaderboard, LeaderboardMember
13+
14+
logger = logging.getLogger("advent_of_code")
15+
16+
17+
class AdventOfCode(commands.GroupCog):
18+
"""Commands for Advent of Code."""
19+
20+
def __init__(self, bot: Friendo):
21+
self.bot = bot
22+
super().__init__()
23+
24+
async def fetch_leaderboard(self, year: int) -> Leaderboard:
25+
"""Get the leaderboard's state for a specified year."""
26+
url = f"https://adventofcode.com/{year}/leaderboard/private/view/{AOC_LEADERBOARD_ID}.json"
27+
cookies = {'session': AOC_SESSION_COOKIE}
28+
29+
# AoC Author has requested applications to provide a url to the tool in the User-Agent
30+
headers = {'User-Agent': 'github.com/fisher60/friendo-bot'}
31+
32+
async with self.bot.session.get(url, cookies=cookies, headers=headers) as response:
33+
data = await response.json()
34+
return Leaderboard(**data)
35+
36+
@staticmethod
37+
def _create_leaderboard_message(members: list[LeaderboardMember], amount: int) -> str:
38+
reset = ""
39+
red = ""
40+
green = ""
41+
yellow = ""
42+
43+
get_line_color = cycle([red, green])
44+
45+
formatted_message = f"Here is our top {amount}\n```ansi\n"
46+
for rank, member in enumerate(members[:amount], start=1):
47+
formatted_message += (
48+
f"{next(get_line_color)}{rank:0>2} {yellow}{reset} {member.name} ({member.local_score})\n"
49+
)
50+
51+
formatted_message += "```"
52+
53+
return formatted_message
54+
55+
@app_commands.command()
56+
@app_commands.describe(
57+
year="View the leaderboard from a specific year, defaults to the current year",
58+
amount="How many members to view, defaults to 10"
59+
)
60+
async def leaderboard(
61+
self,
62+
interaction: discord.Interaction,
63+
year: app_commands.Range[int, 2015, None] = None,
64+
amount: app_commands.Range[int, 0, None] = 10
65+
) -> None:
66+
""" Get the current Advent of Code leaderboard """
67+
if year is None:
68+
year = datetime.now().year
69+
elif year > datetime.now().year: # Don't allow years that haven't happened yet
70+
await interaction.response.send_message(
71+
f"> Please select a valid year 2015 - {datetime.now().year}",
72+
ephemeral=True
73+
)
74+
return
75+
76+
leaderboard = await self.fetch_leaderboard(year)
77+
78+
all_member = list(filter(attrgetter("local_score"), leaderboard.members.values()))
79+
all_member.sort(key=attrgetter("local_score"), reverse=True)
80+
81+
embed = Embed(
82+
title=f"Advent of Code {leaderboard.year}",
83+
description=self._create_leaderboard_message(all_member, amount),
84+
url="https://adventofcode.com",
85+
colour=Color.gold(),
86+
)
87+
embed.add_field(
88+
name="\u200b",
89+
value="Join the fun with `/advent-of-code join`"
90+
)
91+
92+
await interaction.response.send_message(embed=embed)
93+
94+
@app_commands.command()
95+
async def join(self, interaction: discord.Interaction) -> None:
96+
"""Find out how to join the Advent of Code leaderboard!"""
97+
await interaction.response.send_message(
98+
">>> To join the leaderboard, follow these steps:\n"
99+
"\t1. Log in on https://adventofcode.com\n"
100+
"\t2. Head over to https://adventofcode.com/leaderboard/private\n"
101+
f"\t3. Use this code `{AOC_JOIN_CODE}`",
102+
ephemeral=True
103+
)

bot/cogs/advent_of_code/_types.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
from pydantic import BaseModel
4+
5+
6+
class PartCompletion(BaseModel):
7+
star_index: int
8+
get_star_ts: int
9+
10+
11+
class LeaderboardMember(BaseModel):
12+
id: int
13+
name: str
14+
stars: int
15+
local_score: int
16+
global_score: int
17+
last_star_ts: int
18+
completion_day_level: dict[int, dict[int, PartCompletion]]
19+
20+
21+
class Leaderboard(BaseModel):
22+
owner_id: int
23+
event: int
24+
members: dict[int, LeaderboardMember]
25+
26+
@property
27+
def year(self) -> int:
28+
return self.event

bot/settings.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222
AOC_SESSION_COOKIE = environ.get("AOC_SESSION_COOKIE")
2323
AOC_JOIN_CODE = environ.get("AOC_JOIN_CODE")
24+
if AOC_JOIN_CODE:
25+
AOC_LEADERBOARD_ID = AOC_JOIN_CODE.split("-")[0]
26+
else:
27+
AOC_LEADERBOARD_ID = None
2428

2529
FRIENDO_API_USER = environ.get("FRIENDO_API_USER")
2630
FRIENDO_API_PASS = environ.get("FRIENDO_API_PASS")
@@ -34,6 +38,4 @@
3438

3539
GITHUB_REPO = "https://github.com/fisher60/friendo-bot"
3640

37-
AOC_LEADERBOARD_LINK = "https://adventofcode.com/2021/leaderboard/private/view/991705.json"
38-
3941
API_COGS = ["events", "memes"]

0 commit comments

Comments
 (0)