diff --git a/requirements.txt b/requirements.txt index 433048c..e7438f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ markdownify==1.1.0 motor==3.7.1 multidict==6.4.4 opentelemetry-api==1.34.1 -pillow==11.2.1 +pillow==11.3.0 propcache==0.3.2 proto-plus==1.26.1 protobuf==6.31.1 diff --git a/src/bot.py b/src/bot.py index 702ab67..4cd1a4d 100644 --- a/src/bot.py +++ b/src/bot.py @@ -20,8 +20,9 @@ import logging import os import platform +from collections import defaultdict from dataclasses import dataclass -from typing import Any +from typing import Any, DefaultDict import aiohttp import discord @@ -30,7 +31,7 @@ from discord.ext import commands from html2image import Html2Image -from src.constants import GLOBAL_LEADERBOARD_ID +from src.constants import GLOBAL_LEADERBOARD_ID, ProblemList from src.database.models import Profile, Server from src.database.setup import initialise_mongodb_connection from src.utils.channel_logging import DiscordChannelLogger @@ -81,6 +82,9 @@ def __init__( self.channel_logger = DiscordChannelLogger(self, self.config.LOGGING_CHANNEL_ID) self.ratings = Ratings(self) self.neetcode = NeetcodeSolutions(self) + # {problem_list: {problem_id,}} + self.problem_lists: DefaultDict[ProblemList, set[str]] = defaultdict(set) + self._http_client: HttpClient | None = None self._topggpy: topgg.client.DBLClient | None = None diff --git a/src/cogs/problems.py b/src/cogs/problems.py index b3f2e6a..5e5c3df 100644 --- a/src/cogs/problems.py +++ b/src/cogs/problems.py @@ -5,7 +5,7 @@ from discord import app_commands from discord.ext import commands -from src.constants import Difficulty +from src.constants import Difficulty, ProblemList from src.middleware import defer_interaction from src.ui.embeds.problems import ( daily_question_embed, @@ -26,6 +26,12 @@ class DifficultyField(Enum): Hard = Difficulty.HARD Random = Difficulty.RANDOM + class ProblemListField(Enum): + Blind_75 = ProblemList.BLIND_75 + NeetCode_150 = ProblemList.NEETCODE_150 + NeetCode_250 = ProblemList.NEETCODE_250 + NeetCode_All = ProblemList.NEETCODE_ALL + def __init__(self, bot: "DiscordBot") -> None: self.bot = bot @@ -60,13 +66,17 @@ async def random_problem( self, interaction: discord.Interaction, difficulty: DifficultyField = DifficultyField.Random, + problem_list: ProblemListField | None = None, ) -> None: """ Get a random LeetCode problem of your chosen difficulty :param difficulty: The desired difficulty level + :param problem_list: Blind 75, NeetCode 150, NeetCode 250, etc... """ - embed = await random_question_embed(self.bot, difficulty.value) + embed = await random_question_embed( + self.bot, difficulty.value, problem_list.value if problem_list else None + ) await interaction.followup.send(embed=embed) diff --git a/src/constants.py b/src/constants.py index b8296d8..4f2839e 100644 --- a/src/constants.py +++ b/src/constants.py @@ -52,6 +52,21 @@ class Language(Enum): DART = "dart" +class ProblemList(Enum): + BLIND_75 = "blind_75" + NEETCODE_150 = "neetcode_150" + NEETCODE_250 = "neetcode_250" + NEETCODE_ALL = "neetcode_all" + + +NeetCodeBasedProblemList = { + ProblemList.BLIND_75, + ProblemList.NEETCODE_150, + ProblemList.NEETCODE_250, + ProblemList.NEETCODE_ALL, +} + + class StatsCardExtensions(Enum): ACTIVITY = "activity" HEATMAP = "heatmap" diff --git a/src/ui/embeds/problems.py b/src/ui/embeds/problems.py index 4fb00ba..4b4021a 100644 --- a/src/ui/embeds/problems.py +++ b/src/ui/embeds/problems.py @@ -1,8 +1,9 @@ +import random from typing import TYPE_CHECKING import discord -from src.constants import Difficulty +from src.constants import Difficulty, NeetCodeBasedProblemList, ProblemList from src.ui.embeds.common import failure_embed from src.utils.problems import ( fetch_daily_question, @@ -37,9 +38,18 @@ async def search_question_embed(bot: "DiscordBot", search_text: str) -> discord. async def random_question_embed( - bot: "DiscordBot", difficulty: Difficulty + bot: "DiscordBot", + difficulty: Difficulty, + problem_list: ProblemList | None = None, ) -> discord.Embed: - question_title = await fetch_random_question(bot, difficulty) + question_title: str | None = None + + if not problem_list: + question_title = await fetch_random_question(bot, difficulty) + elif problem_list in NeetCodeBasedProblemList and problem_list in bot.problem_lists: + question_title = random.choice(tuple(bot.problem_lists[problem_list])) + else: + raise NotImplementedError if not question_title: return question_error_embed() @@ -48,7 +58,10 @@ async def random_question_embed( return embed -async def question_embed(bot: "DiscordBot", question_title: str) -> discord.Embed: +async def question_embed( + bot: "DiscordBot", + question_title: str, +) -> discord.Embed: info = await fetch_question_info(bot, question_title) if not info: @@ -87,9 +100,28 @@ async def question_embed(bot: "DiscordBot", question_title: str) -> discord.Embe name="Zerotrac Rating: ", value=f"||{info.question_rating}||", inline=True ) + problem_lists_in = sorted( + [ + problem_list + for problem_list, question_titles in bot.problem_lists.items() + if question_title in question_titles + ], + key=lambda problem_list: problem_list.value, + ) + + problem_lists_text = ", ".join( + problem_list.value.replace("_", " ").title() + for problem_list in problem_lists_in + ) + embed.set_footer( - text=f"Accepted: {info.total_accepted} | Submissions: " - f"{info.total_submission} | Acceptance Rate: {info.ac_rate}" + text=( + f"Accepted: {info.total_accepted} | Submissions: " + f"{info.total_submission} | Acceptance Rate: {info.ac_rate}" + f"\n\nThis problem is part of: {problem_lists_text}" + if problem_lists_in + else "" + ) ) return embed diff --git a/src/utils/neetcode.py b/src/utils/neetcode.py index 3b1a52c..2f6d85f 100644 --- a/src/utils/neetcode.py +++ b/src/utils/neetcode.py @@ -1,9 +1,10 @@ import json import re +from collections import defaultdict from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, DefaultDict -from src.constants import Language +from src.constants import Language, ProblemList if TYPE_CHECKING: # To prevent circular imports @@ -19,8 +20,9 @@ class NeetcodeSolution: difficulty: str video: str code: str - neetcode150: bool = False blind75: bool = False + neetcode150: bool = False + neetcode250: bool = False class NeetcodeSolutions: @@ -28,22 +30,25 @@ def __init__(self, bot: "DiscordBot") -> None: self.bot = bot # {link/question-title-slug: NeetcodeSolution} self.solutions: dict[str, NeetcodeSolution] = {} + # {problem_list: {problem_id,}} + self.problem_lists: DefaultDict[ProblemList, set[str]] = defaultdict(set) async def update_solutions(self) -> None: """ Updates the solutions. """ - self.solutions = await self._fetch_neetcode_solutions() + self.solutions = await self.__fetch_neetcode_solutions() + self.__add_problem_lists() self.bot.logger.info("Updated NeetCode solutions") - async def _fetch_neetcode_solutions(self) -> dict[str, NeetcodeSolution]: + async def __fetch_neetcode_solutions(self) -> dict[str, NeetcodeSolution]: """ Fetches the NeetCode solutions data. :return: The dictionary mapping from link/question-title-slug to `NeetcodeSolution`. """ - main_js_filename = await self._retrieve_neetcode_main_js_filename() + main_js_filename = await self.__retrieve_neetcode_main_js_filename() if not ( response_data := await self.bot.http_client.fetch_data( @@ -55,7 +60,7 @@ async def _fetch_neetcode_solutions(self) -> dict[str, NeetcodeSolution]: solutions = self._parse_main_js(response_data) return solutions - async def _retrieve_neetcode_main_js_filename(self) -> str | None: + async def __retrieve_neetcode_main_js_filename(self) -> str | None: """ Fetches https://neetcode.io/ and retrieves the filename of the main.[a-z0-9]{16}.js file. @@ -107,8 +112,9 @@ def _parse_main_js(self, main_js: str) -> dict[str, NeetcodeSolution]: difficulty=solution["difficulty"], video=solution["video"], code=solution["code"], - neetcode150=True if "neetcode150" in solution else False, - blind75=True if "blind75" in solution else False, + blind75="blind75" in solution, + neetcode150="neetcode150" in solution, + neetcode250="neetcode250" in solution, ) except ValueError as e: self.bot.logger.exception( @@ -117,6 +123,19 @@ def _parse_main_js(self, main_js: str) -> dict[str, NeetcodeSolution]: return link_to_solution + def __add_problem_lists(self) -> None: + """Adds the NeetCode based problem lists to the bot's problem lists.""" + + for problem in self.solutions.values(): + if problem.blind75: + self.bot.problem_lists[ProblemList.BLIND_75].add(problem.title) + if problem.neetcode150: + self.bot.problem_lists[ProblemList.NEETCODE_150].add(problem.title) + if problem.neetcode250: + self.bot.problem_lists[ProblemList.NEETCODE_250].add(problem.title) + + self.bot.problem_lists[ProblemList.NEETCODE_ALL].add(problem.title) + def neetcode_solution_github_link(github_code_filename: str, language: Language) -> str: """