From 838cb5d04e9469550d91a4954084cbd00bb965a9 Mon Sep 17 00:00:00 2001 From: Kevin Roman <44275689+Kevin-Roman@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:11:59 +0100 Subject: [PATCH 1/4] chore(requirements): upgrade `pillow` to `v11.3.0` There is a heap buffer overflow when writing a sufficiently large (>64k encoded with default settings) image in the DDS format due to writing into a buffer without checking for available space. This only affects users who save untrusted data as a compressed DDS image. - Unclear how large the potential write could be. It is likely limited by process segfault, so it's not necessarily deterministic. It may be practically unbounded. - Unclear if there's a restriction on the bytes that could be emitted. It's likely that the only restriction is that the bytes would be emitted in chunks of 8 or 16. This was introduced in Pillow 11.2.0 when the feature was added. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e13b9bd116d18c3a3163263709fd41db59763b1c Mon Sep 17 00:00:00 2001 From: Kevin Roman <44275689+Kevin-Roman@users.noreply.github.com> Date: Sun, 13 Jul 2025 22:20:44 +0100 Subject: [PATCH 2/4] feat(utils/problems): add `neetcode250` check --- src/utils/neetcode.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/neetcode.py b/src/utils/neetcode.py index 3b1a52c..3dd4092 100644 --- a/src/utils/neetcode.py +++ b/src/utils/neetcode.py @@ -19,8 +19,9 @@ class NeetcodeSolution: difficulty: str video: str code: str - neetcode150: bool = False blind75: bool = False + neetcode150: bool = False + neetcode250: bool = False class NeetcodeSolutions: @@ -107,8 +108,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, + neetcode150=True if "neetcode150" in solution else False, + neetcode250=True if "neetcode250" in solution else False, ) except ValueError as e: self.bot.logger.exception( From f19e3ce39a593a0d340568fb8e129594c63181e8 Mon Sep 17 00:00:00 2001 From: Kevin Roman <44275689+Kevin-Roman@users.noreply.github.com> Date: Sun, 13 Jul 2025 23:27:29 +0100 Subject: [PATCH 3/4] feat(src/utils): store and random selection problem lists option --- src/cogs/problems.py | 16 ++++++- src/constants.py | 87 +++++++++++++++++++++++++++++++++++++++ src/ui/embeds/problems.py | 19 +++++++-- src/utils/neetcode.py | 34 ++++++++++++--- 4 files changed, 145 insertions(+), 11 deletions(-) diff --git a/src/cogs/problems.py b/src/cogs/problems.py index b3f2e6a..0d0162d 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,14 @@ class DifficultyField(Enum): Hard = Difficulty.HARD Random = Difficulty.RANDOM + class ProblemListField(Enum): + LeetCode_All = ProblemList.LEETCODE_ALL + Grind_75 = ProblemList.GRIND_75 + 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 +68,17 @@ async def random_problem( self, interaction: discord.Interaction, difficulty: DifficultyField = DifficultyField.Random, + problem_list: ProblemListField = ProblemListField.LeetCode_All, ) -> None: """ Get a random LeetCode problem of your chosen difficulty :param difficulty: The desired difficulty level + :param problem_list: Grind 75, Blind 75, NeetCode 150, etc... """ - embed = await random_question_embed(self.bot, difficulty.value) + embed = await random_question_embed( + self.bot, difficulty.value, problem_list.value + ) await interaction.followup.send(embed=embed) diff --git a/src/constants.py b/src/constants.py index b8296d8..c0eb4c5 100644 --- a/src/constants.py +++ b/src/constants.py @@ -52,6 +52,15 @@ class Language(Enum): DART = "dart" +class ProblemList(Enum): + LEETCODE_ALL = "leetcode_all" + GRIND_75 = "grind_75" + BLIND_75 = "blind_75" + NEETCODE_150 = "neetcode_150" + NEETCODE_250 = "neetcode_250" + NEETCODE_ALL = "neetcode_all" + + class StatsCardExtensions(Enum): ACTIVITY = "activity" HEATMAP = "heatmap" @@ -190,3 +199,81 @@ class CodeGrindTierInfo: } VERIFIED_ROLE = "CodeGrind Verified" + +GRIND_75_QUESTION_TITLES = { + "two-sum", + "longest-substring-without-repeating-characters", + "longest-palindromic-substring", + "clone-graph", + "ransom-note", + "string-to-integer-atoi", + "container-with-most-water", + "word-break", + "linked-list-cycle", + "middle-of-the-linked-list", + "3sum", + "rotting-oranges", + "letter-combinations-of-a-phone-number", + "lru-cache", + "valid-parentheses", + "merge-two-sorted-lists", + "evaluate-reverse-polish-notation", + "merge-k-sorted-lists", + "first-bad-version", + "longest-palindrome", + "binary-search", + "min-stack", + "time-based-key-value-store", + "01-matrix", + "diameter-of-binary-tree", + "partition-equal-subset-sum", + "search-in-rotated-sorted-array", + "combination-sum", + "find-median-from-data-stream", + "majority-element", + "trapping-rain-water", + "serialize-and-deserialize-binary-tree", + "permutations", + "maximum-subarray", + "spiral-matrix", + "minimum-height-trees", + "merge-intervals", + "insert-interval", + "find-all-anagrams-in-a-string", + "unique-paths", + "coin-change", + "add-binary", + "climbing-stairs", + "binary-tree-right-side-view", + "number-of-islands", + "maximum-profit-in-job-scheduling", + "sort-colors", + "minimum-window-substring", + "subsets", + "word-search", + "reverse-linked-list", + "course-schedule", + "implement-trie-prefix-tree", + "accounts-merge", + "largest-rectangle-in-histogram", + "contains-duplicate", + "flood-fill", + "basic-calculator", + "validate-binary-search-tree", + "invert-binary-tree", + "binary-tree-level-order-traversal", + "kth-smallest-element-in-a-bst", + "maximum-depth-of-binary-tree", + "construct-binary-tree-from-preorder-and-inorder-traversal", + "implement-queue-using-stacks", + "lowest-common-ancestor-of-a-binary-search-tree", + "lowest-common-ancestor-of-a-binary-tree", + "task-scheduler", + "balanced-binary-tree", + "product-of-array-except-self", + "valid-anagram", + "k-closest-points-to-origin", + "best-time-to-buy-and-sell-stock", + "valid-palindrome", + "word-ladder", +) diff --git a/src/ui/embeds/problems.py b/src/ui/embeds/problems.py index 4fb00ba..30a0c29 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, ProblemList, GRIND_75_QUESTION_TITLES from src.ui.embeds.common import failure_embed from src.utils.problems import ( fetch_daily_question, @@ -37,9 +38,21 @@ 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 = ProblemList.LEETCODE_ALL, ) -> discord.Embed: - question_title = await fetch_random_question(bot, difficulty) + question_title: str | None = None + + match problem_list: + case ProblemList.LEETCODE_ALL: + question_title = await fetch_random_question(bot, difficulty) + case ProblemList.GRIND_75: + question_title = random.choice(GRIND_75_QUESTION_TITLES) + case _: + question_title = random.choice( + tuple(bot.neetcode.problem_lists[problem_list]) + ) if not question_title: return question_error_embed() diff --git a/src/utils/neetcode.py b/src/utils/neetcode.py index 3dd4092..881ccac 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 @@ -29,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.problem_lists = self.__get_problem_lists(self.solutions) 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( @@ -56,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. @@ -119,6 +123,24 @@ def _parse_main_js(self, main_js: str) -> dict[str, NeetcodeSolution]: return link_to_solution + @staticmethod + def __get_problem_lists( + solutions: dict[str, NeetcodeSolution], + ) -> DefaultDict[ProblemList, set[str]]: + problem_lists: DefaultDict[ProblemList, set[str]] = defaultdict(set) + + for problem in solutions.values(): + if problem.blind75: + problem_lists[ProblemList.BLIND_75].add(problem.title) + if problem.neetcode150: + problem_lists[ProblemList.NEETCODE_150].add(problem.title) + if problem.neetcode250: + problem_lists[ProblemList.NEETCODE_250].add(problem.title) + + problem_lists[ProblemList.NEETCODE_ALL].add(problem.title) + + return problem_lists + def neetcode_solution_github_link(github_code_filename: str, language: Language) -> str: """ From c101905809013bf14443000d3aeb3b3cc644386b Mon Sep 17 00:00:00 2001 From: Kevin Roman <44275689+Kevin-Roman@users.noreply.github.com> Date: Mon, 14 Jul 2025 00:43:20 +0100 Subject: [PATCH 4/4] feat(utils/neetcode): mention problem list in problem embed footer --- src/bot.py | 8 +++- src/cogs/problems.py | 8 ++-- src/constants.py | 88 ++++----------------------------------- src/ui/embeds/problems.py | 47 ++++++++++++++------- src/utils/neetcode.py | 27 +++++------- 5 files changed, 61 insertions(+), 117 deletions(-) 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 0d0162d..5e5c3df 100644 --- a/src/cogs/problems.py +++ b/src/cogs/problems.py @@ -27,8 +27,6 @@ class DifficultyField(Enum): Random = Difficulty.RANDOM class ProblemListField(Enum): - LeetCode_All = ProblemList.LEETCODE_ALL - Grind_75 = ProblemList.GRIND_75 Blind_75 = ProblemList.BLIND_75 NeetCode_150 = ProblemList.NEETCODE_150 NeetCode_250 = ProblemList.NEETCODE_250 @@ -68,16 +66,16 @@ async def random_problem( self, interaction: discord.Interaction, difficulty: DifficultyField = DifficultyField.Random, - problem_list: ProblemListField = ProblemListField.LeetCode_All, + problem_list: ProblemListField | None = None, ) -> None: """ Get a random LeetCode problem of your chosen difficulty :param difficulty: The desired difficulty level - :param problem_list: Grind 75, Blind 75, NeetCode 150, etc... + :param problem_list: Blind 75, NeetCode 150, NeetCode 250, etc... """ embed = await random_question_embed( - self.bot, difficulty.value, problem_list.value + 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 c0eb4c5..4f2839e 100644 --- a/src/constants.py +++ b/src/constants.py @@ -53,14 +53,20 @@ class Language(Enum): class ProblemList(Enum): - LEETCODE_ALL = "leetcode_all" - GRIND_75 = "grind_75" 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" @@ -199,81 +205,3 @@ class CodeGrindTierInfo: } VERIFIED_ROLE = "CodeGrind Verified" - -GRIND_75_QUESTION_TITLES = { - "two-sum", - "longest-substring-without-repeating-characters", - "longest-palindromic-substring", - "clone-graph", - "ransom-note", - "string-to-integer-atoi", - "container-with-most-water", - "word-break", - "linked-list-cycle", - "middle-of-the-linked-list", - "3sum", - "rotting-oranges", - "letter-combinations-of-a-phone-number", - "lru-cache", - "valid-parentheses", - "merge-two-sorted-lists", - "evaluate-reverse-polish-notation", - "merge-k-sorted-lists", - "first-bad-version", - "longest-palindrome", - "binary-search", - "min-stack", - "time-based-key-value-store", - "01-matrix", - "diameter-of-binary-tree", - "partition-equal-subset-sum", - "search-in-rotated-sorted-array", - "combination-sum", - "find-median-from-data-stream", - "majority-element", - "trapping-rain-water", - "serialize-and-deserialize-binary-tree", - "permutations", - "maximum-subarray", - "spiral-matrix", - "minimum-height-trees", - "merge-intervals", - "insert-interval", - "find-all-anagrams-in-a-string", - "unique-paths", - "coin-change", - "add-binary", - "climbing-stairs", - "binary-tree-right-side-view", - "number-of-islands", - "maximum-profit-in-job-scheduling", - "sort-colors", - "minimum-window-substring", - "subsets", - "word-search", - "reverse-linked-list", - "course-schedule", - "implement-trie-prefix-tree", - "accounts-merge", - "largest-rectangle-in-histogram", - "contains-duplicate", - "flood-fill", - "basic-calculator", - "validate-binary-search-tree", - "invert-binary-tree", - "binary-tree-level-order-traversal", - "kth-smallest-element-in-a-bst", - "maximum-depth-of-binary-tree", - "construct-binary-tree-from-preorder-and-inorder-traversal", - "implement-queue-using-stacks", - "lowest-common-ancestor-of-a-binary-search-tree", - "lowest-common-ancestor-of-a-binary-tree", - "task-scheduler", - "balanced-binary-tree", - "product-of-array-except-self", - "valid-anagram", - "k-closest-points-to-origin", - "best-time-to-buy-and-sell-stock", - "valid-palindrome", - "word-ladder", -) diff --git a/src/ui/embeds/problems.py b/src/ui/embeds/problems.py index 30a0c29..4b4021a 100644 --- a/src/ui/embeds/problems.py +++ b/src/ui/embeds/problems.py @@ -3,7 +3,7 @@ import discord -from src.constants import Difficulty, ProblemList, GRIND_75_QUESTION_TITLES +from src.constants import Difficulty, NeetCodeBasedProblemList, ProblemList from src.ui.embeds.common import failure_embed from src.utils.problems import ( fetch_daily_question, @@ -40,19 +40,16 @@ async def search_question_embed(bot: "DiscordBot", search_text: str) -> discord. async def random_question_embed( bot: "DiscordBot", difficulty: Difficulty, - problem_list: ProblemList = ProblemList.LEETCODE_ALL, + problem_list: ProblemList | None = None, ) -> discord.Embed: question_title: str | None = None - match problem_list: - case ProblemList.LEETCODE_ALL: - question_title = await fetch_random_question(bot, difficulty) - case ProblemList.GRIND_75: - question_title = random.choice(GRIND_75_QUESTION_TITLES) - case _: - question_title = random.choice( - tuple(bot.neetcode.problem_lists[problem_list]) - ) + 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() @@ -61,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: @@ -100,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 881ccac..2f6d85f 100644 --- a/src/utils/neetcode.py +++ b/src/utils/neetcode.py @@ -38,7 +38,7 @@ async def update_solutions(self) -> None: Updates the solutions. """ self.solutions = await self.__fetch_neetcode_solutions() - self.problem_lists = self.__get_problem_lists(self.solutions) + self.__add_problem_lists() self.bot.logger.info("Updated NeetCode solutions") async def __fetch_neetcode_solutions(self) -> dict[str, NeetcodeSolution]: @@ -112,9 +112,9 @@ def _parse_main_js(self, main_js: str) -> dict[str, NeetcodeSolution]: difficulty=solution["difficulty"], video=solution["video"], code=solution["code"], - blind75=True if "blind75" in solution else False, - neetcode150=True if "neetcode150" in solution else False, - neetcode250=True if "neetcode250" in solution else False, + blind75="blind75" in solution, + neetcode150="neetcode150" in solution, + neetcode250="neetcode250" in solution, ) except ValueError as e: self.bot.logger.exception( @@ -123,23 +123,18 @@ def _parse_main_js(self, main_js: str) -> dict[str, NeetcodeSolution]: return link_to_solution - @staticmethod - def __get_problem_lists( - solutions: dict[str, NeetcodeSolution], - ) -> DefaultDict[ProblemList, set[str]]: - problem_lists: DefaultDict[ProblemList, set[str]] = defaultdict(set) + def __add_problem_lists(self) -> None: + """Adds the NeetCode based problem lists to the bot's problem lists.""" - for problem in solutions.values(): + for problem in self.solutions.values(): if problem.blind75: - problem_lists[ProblemList.BLIND_75].add(problem.title) + self.bot.problem_lists[ProblemList.BLIND_75].add(problem.title) if problem.neetcode150: - problem_lists[ProblemList.NEETCODE_150].add(problem.title) + self.bot.problem_lists[ProblemList.NEETCODE_150].add(problem.title) if problem.neetcode250: - problem_lists[ProblemList.NEETCODE_250].add(problem.title) + self.bot.problem_lists[ProblemList.NEETCODE_250].add(problem.title) - problem_lists[ProblemList.NEETCODE_ALL].add(problem.title) - - return problem_lists + self.bot.problem_lists[ProblemList.NEETCODE_ALL].add(problem.title) def neetcode_solution_github_link(github_code_filename: str, language: Language) -> str: