Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
14 changes: 12 additions & 2 deletions src/cogs/problems.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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)


Expand Down
15 changes: 15 additions & 0 deletions src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
44 changes: 38 additions & 6 deletions src/ui/embeds/problems.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
37 changes: 28 additions & 9 deletions src/utils/neetcode.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,31 +20,35 @@ class NeetcodeSolution:
difficulty: str
video: str
code: str
neetcode150: bool = False
blind75: bool = False
neetcode150: bool = False
neetcode250: bool = False


class NeetcodeSolutions:
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(
Expand All @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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:
"""
Expand Down