Skip to content

Commit f4deb73

Browse files
committed
chore(release): v1.0.0 - initial working version of LeetCode status checker bot
1 parent 8ecc3e6 commit f4deb73

File tree

6 files changed

+456
-0
lines changed

6 files changed

+456
-0
lines changed

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.env
2+
.zip
3+
users.json
4+
__pycache__/
5+
*.pyc
6+
.vscode/
7+
.env.local
8+
.env.*.local
9+
.DS_Store
10+
.idea/
11+
*.iml
12+
logs/
13+
*.log

main.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import schedule
2+
import time
3+
import sys
4+
import threading
5+
import itertools
6+
from src.core_logic import run_check
7+
from src.config import validate_config
8+
from tqdm import tqdm
9+
import time
10+
from rich.console import Console
11+
from rich.panel import Panel
12+
from rich import print as rprint
13+
14+
spinner_running = True
15+
spinner_cycle = itertools.cycle(['-', '\\', '|', '/'])
16+
17+
def spinner_thread():
18+
"""A simple thread to show a spinning cursor in the console."""
19+
while spinner_running:
20+
sys.stdout.write(next(spinner_cycle) + '\r')
21+
sys.stdout.flush()
22+
time.sleep(0.1)
23+
sys.stdout.write(' \r')
24+
sys.stdout.flush()
25+
26+
def schedule_jobs():
27+
"""Sets up the scheduled times for the submission check."""
28+
# Note: These times are UTC, which is standard for servers.
29+
utc_times = ["03:30", "06:30", "13:30", "17:30"]
30+
print(f"⏰ Scheduling jobs at the following UTC times: {', '.join(utc_times)}")
31+
for t in utc_times:
32+
schedule.every().day.at(t).do(run_check)
33+
34+
def main():
35+
"""Main function to start the bot."""
36+
global spinner_running
37+
console = Console()
38+
39+
# startup banner
40+
console.print(Panel.fit(
41+
"[bold yellow]LeetCode Reminder Bot[/bold yellow]\n\n"
42+
"[blue] Version 1.0[/blue]",
43+
title="🤖 Welcome",
44+
border_style="green",
45+
padding=(1, 5)
46+
))
47+
#loading animation
48+
with tqdm(total=100,
49+
desc="Initializing system",
50+
colour='cyan',
51+
ncols=75,
52+
bar_format='{desc}: {percentage:3.0f}%|{bar}| {n_fmt}/{total_fmt}') as pbar:
53+
for i in range(10):
54+
time.sleep(0.1)
55+
pbar.update(10)
56+
57+
if not validate_config():
58+
sys.exit(1) # Exit if secrets aren't set
59+
60+
# Run one check immediately on startup
61+
try:
62+
run_check()
63+
except Exception as e:
64+
print(f"\nAn error occurred during the initial run: {e}")
65+
66+
schedule_jobs()
67+
print("\n Scheduler is now running. Waiting for the next scheduled time...")
68+
print(" (Running as a service. Press Ctrl+C to exit if running locally.)")
69+
70+
t = threading.Thread(target=spinner_thread)
71+
t.start()
72+
73+
try:
74+
while True:
75+
schedule.run_pending()
76+
time.sleep(1)
77+
except KeyboardInterrupt:
78+
print("\n")
79+
rprint(Panel.fit(
80+
"[bold yellow]Shutting down the bot[/bold yellow]\n",
81+
title=" Goodbye! :(",
82+
border_style="cyan",
83+
padding=(1, 4)
84+
))
85+
spinner_running = False
86+
t.join()
87+
88+
if __name__ == "__main__":
89+
main()

src/config.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
import json
3+
from dotenv import load_dotenv
4+
5+
# Load environment variables from a .env file if it exists
6+
# This is great for local development.
7+
# On VM, we will set these variables directly.
8+
load_dotenv()
9+
10+
# --- Load Environment Variables ---
11+
SMTP_USER = os.getenv("SMTP_USER")
12+
SMTP_PASSWORD = os.getenv("GMAIL_APP_PASSWORD")
13+
14+
15+
SMTP_SERVER = os.getenv("SMTP_SERVER", "smtp.gmail.com")
16+
SMTP_PORT = int(os.getenv("SMTP_PORT", 587))
17+
LEETCODE_API_URL = os.getenv("LEETCODE_API_URL", "https://leetcode.com/graphql")
18+
19+
# --- Static Config (App-level constants) ---
20+
QUOTES = [
21+
"Keep pushing! You’re closer than you think 💪",
22+
"Consistency beats motivation every time ⚡",
23+
"Another problem solved — another step ahead 🚀",
24+
"Your hard work today is your success tomorrow 💫",
25+
"One problem a day keeps the bugs away 🧠"
26+
]
27+
28+
# --- GraphQL Queries ---
29+
QUERY_DAILY_QUESTION = """
30+
query questionOfToday {
31+
activeDailyCodingChallengeQuestion {
32+
date
33+
link
34+
question {
35+
title
36+
titleSlug
37+
difficulty
38+
hints
39+
topicTags{
40+
name
41+
}
42+
}
43+
}
44+
}
45+
"""
46+
47+
QUERY_RECENT_SUBMISSIONS = """
48+
query recentAcSubmissions($username: String!, $limit: Int!) {
49+
recentAcSubmissionList(username: $username, limit: $limit) {
50+
titleSlug
51+
timestamp
52+
}
53+
}
54+
"""
55+
56+
# --- Helper Functions ---
57+
58+
def load_users():
59+
"""Loads the user list from users.json."""
60+
users_filepath = 'users.json' # place users.json in the root
61+
if not os.path.exists(users_filepath):
62+
# local testing
63+
users_filepath = os.path.join(os.path.dirname(__file__), '..', 'users.json')
64+
65+
try:
66+
with open(users_filepath, 'r') as f:
67+
return json.load(f)
68+
except FileNotFoundError:
69+
print(" users.json file not found at {users_filepath}.")
70+
return []
71+
except json.JSONDecodeError:
72+
print(f" Error: {users_filepath} is not valid JSON.")
73+
return []
74+
75+
def validate_config():
76+
"""Checks if essential secrets are loaded."""
77+
if not SMTP_USER or not SMTP_PASSWORD:
78+
print(" FATAL ERROR: SMTP_USER or GMAIL_APP_PASSWORD is not set.")
79+
print(" Please set these environment variables before running.")
80+
print(" If running locally, check your .env file.")
81+
return False
82+
83+
print("Configuration and secrets loaded successfully.")
84+
return True
85+

src/core_logic.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import random
2+
from datetime import datetime, timedelta
3+
from . import config
4+
from . import leetcode_api
5+
from halo import Halo
6+
from . import email_service
7+
8+
def run_check():
9+
"""Main logic to check submissions for each user and send emails."""
10+
ist_now = datetime.utcnow() + timedelta(hours=5, minutes=30)
11+
print(f"\n[{ist_now.strftime('%Y-%m-%d %H:%M:%S')} IST] Starting submission check...")
12+
13+
daily_slug, question_link = leetcode_api.get_daily_question()
14+
if not daily_slug:
15+
print("Aborting check since daily question could not be fetched.")
16+
return
17+
18+
print(f"Today's POTD is '{daily_slug}'")
19+
today = ist_now.date()
20+
21+
users = config.load_users()
22+
if not users:
23+
print("No users loaded from users.json. Exiting check.")
24+
return
25+
26+
for user in users:
27+
username, email = user["username"], user["email"]
28+
print(f"\n🔍 Checking user: {username}")
29+
30+
#submissions = leetcode_api.get_recent_submissions(username)
31+
with Halo(text='Fetching submissions...', spinner='star2', color='cyan') as spinner:
32+
submissions = leetcode_api.get_recent_submissions(username)
33+
spinner.succeed('Submissions fetched successfully!')
34+
35+
solved_today = any(
36+
sub["titleSlug"] == daily_slug and
37+
(datetime.fromtimestamp(int(sub["timestamp"])) + timedelta(hours=5, minutes=30)).date() == today
38+
for sub in submissions
39+
)
40+
41+
if solved_today:
42+
print(f"[ {username} ] has already solved the daily problem.")
43+
quote = random.choice(config.QUOTES)
44+
html = email_service.build_html_email(username, daily_slug, question_link, True, quote)
45+
subject = f"Awesome! You solved today’s LeetCode challenge!"
46+
else:
47+
print(f" [ {username} ]has not solved the daily problem yet. Sending reminder.")
48+
html = email_service.build_html_email(username, daily_slug, question_link, False)
49+
subject = f"⏳ Reminder: Solve Today’s LeetCode Problem!"
50+
51+
52+
with Halo(text='Sending mail..', spinner='bouncingBar', color='yellow') as spinner:
53+
try:
54+
email_service.send_email(email, subject, html)
55+
spinner.succeed(f"Mail sent successfully! to user {username}")
56+
except Exception as e:
57+
spinner.fail(f'Failed to send mail: {e}')
58+
59+
print("\n--- Check complete ---")

0 commit comments

Comments
 (0)