| import os |
| import re |
| import uuid |
| import pytz |
| import psycopg2 |
| import requests |
| import gradio as gr |
| import json |
| import pickle |
| import base64 |
|
|
| from google.auth.transport.requests import Request |
| from googleapiclient.discovery import build |
| from google.oauth2.credentials import Credentials |
|
|
| from email.mime.text import MIMEText |
| from datetime import datetime, date |
| from dotenv import load_dotenv |
|
|
|
|
| |
|
|
| load_dotenv() |
|
|
| DB_URL = os.getenv("DB_URL") |
| GMAIL_USER = os.getenv("GMAIL_USER") |
| CRON_SECRET = os.getenv("CRON") |
| HF_URL = os.getenv("HF_URL") |
|
|
| LEETCODE_API = "https://leetcode-api-vercel.vercel.app" |
|
|
| SCOPES = ["https://www.googleapis.com/auth/gmail.send"] |
| TOKEN_FILE = "token.pkl" |
|
|
|
|
| |
|
|
| def get_db(): |
| return psycopg2.connect(DB_URL, sslmode="require") |
|
|
|
|
| |
|
|
| def get_gmail_service(): |
|
|
| creds = None |
|
|
| if os.path.exists(TOKEN_FILE): |
| try: |
| with open(TOKEN_FILE, "rb") as f: |
| creds = pickle.load(f) |
| except: |
| creds = None |
|
|
| if creds and creds.expired and creds.refresh_token: |
| try: |
| creds.refresh(Request()) |
|
|
| with open(TOKEN_FILE, "wb") as f: |
| pickle.dump(creds, f) |
|
|
| except: |
| creds = None |
|
|
| if not creds: |
| raise Exception("β token.pkl missing. Upload valid token.") |
|
|
| return build("gmail", "v1", credentials=creds) |
|
|
|
|
| def send_email(to, subject, html): |
|
|
| try: |
| service = get_gmail_service() |
|
|
| msg = MIMEText(html, "html") |
| msg["To"] = to |
| msg["From"] = GMAIL_USER |
| msg["Subject"] = subject |
|
|
| raw = base64.urlsafe_b64encode( |
| msg.as_bytes() |
| ).decode() |
|
|
| body = {"raw": raw} |
|
|
| service.users().messages().send( |
| userId="me", |
| body=body |
| ).execute() |
|
|
| print("β
Sent:", to) |
| return True |
|
|
| except Exception as e: |
| print("β Gmail error:", e) |
| return False |
|
|
|
|
| def create_email_template(title, content, unsubscribe_link, email_type="reminder", problem_link=None, difficulty=None): |
| """Create a beautiful HTML email template""" |
| |
| |
| colors = { |
| "morning": {"primary": "#4CAF50", "secondary": "#81C784", "bg": "#E8F5E8"}, |
| "afternoon": {"primary": "#FF9800", "secondary": "#FFB74D", "bg": "#FFF3E0"}, |
| "night": {"primary": "#F44336", "secondary": "#EF5350", "bg": "#FFEBEE"}, |
| "verification": {"primary": "#2196F3", "secondary": "#64B5F6", "bg": "#E3F2FD"} |
| } |
| |
| color = colors.get(email_type, colors["morning"]) |
| |
| |
| difficulty_colors = { |
| "Easy": "#00B04F", |
| "Medium": "#FFA116", |
| "Hard": "#FF375F" |
| } |
| |
| |
| if email_type == "verification": |
| problem_button = f""" |
| <div style="text-align: center; margin: 30px 0;"> |
| <a href="{unsubscribe_link}" |
| style="display: inline-block; background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); |
| color: white; padding: 15px 30px; text-decoration: none; border-radius: 25px; |
| font-weight: bold; font-size: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); |
| transition: transform 0.2s;"> |
| β
Verify Email |
| </a> |
| </div> |
| """ |
| tips_section = "" |
| motivation_section = "" |
| difficulty_badge = "" |
| else: |
| |
| if problem_link: |
| link_url = problem_link |
| else: |
| link_url = f"https://leetcode.com/problems/{title.lower().replace(' ', '-').replace('.', '')}" |
| |
| |
| if difficulty: |
| diff_color = difficulty_colors.get(difficulty, "#666") |
| difficulty_badge = f""" |
| <div style="text-align: center; margin: 10px 0;"> |
| <span style="background-color: {diff_color}; color: white; padding: 4px 12px; |
| border-radius: 12px; font-size: 12px; font-weight: bold;"> |
| {difficulty} |
| </span> |
| </div> |
| """ |
| else: |
| difficulty_badge = "" |
| |
| problem_button = f""" |
| <div style="text-align: center; margin: 30px 0;"> |
| <a href="{link_url}" |
| style="display: inline-block; background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); |
| color: white; padding: 15px 30px; text-decoration: none; border-radius: 25px; |
| font-weight: bold; font-size: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); |
| transition: transform 0.2s;"> |
| π Solve Problem |
| </a> |
| </div> |
| """ |
| |
| |
| if difficulty == "Easy": |
| tips_content = """ |
| <li>Focus on understanding the problem clearly</li> |
| <li>Think about the simplest approach first</li> |
| <li>Test with the given examples</li> |
| <li>Consider edge cases like empty inputs</li> |
| """ |
| elif difficulty == "Medium": |
| tips_content = """ |
| <li>Break the problem into smaller subproblems</li> |
| <li>Consider multiple approaches (greedy, DP, etc.)</li> |
| <li>Think about time and space complexity</li> |
| <li>Use appropriate data structures</li> |
| """ |
| else: |
| tips_content = """ |
| <li>Study similar problems and patterns</li> |
| <li>Don't rush - take time to understand</li> |
| <li>Consider advanced algorithms and techniques</li> |
| <li>Break it down step by step</li> |
| """ |
| |
| tips_section = f""" |
| <!-- Tips Section --> |
| <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0;"> |
| <h3 style="color: #333; margin: 0 0 15px 0; font-size: 18px;">π‘ {difficulty} Problem Tips</h3> |
| <ul style="color: #666; margin: 0; padding-left: 20px; line-height: 1.6;"> |
| {tips_content} |
| </ul> |
| </div> |
| """ |
| |
| motivation_quotes = [ |
| "The expert in anything was once a beginner.", |
| "Every problem is a step forward in your journey.", |
| "Consistency beats perfection every time.", |
| "Code today, conquer tomorrow.", |
| "Small progress is still progress." |
| ] |
| import random |
| quote = random.choice(motivation_quotes) |
| |
| motivation_section = f""" |
| <!-- Stats or Motivation --> |
| <div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; margin: 25px 0;"> |
| <p style="color: white; margin: 0; font-size: 16px; font-style: italic;"> |
| "{quote}" πͺ |
| </p> |
| </div> |
| """ |
| |
| return f""" |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>LeetCode Daily Tracker</title> |
| </head> |
| <body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f5f5f5;"> |
| <div style="max-width: 600px; margin: 0 auto; background-color: white; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"> |
| |
| <!-- Header --> |
| <div style="background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); padding: 30px; text-align: center;"> |
| <h1 style="color: white; margin: 0; font-size: 28px; font-weight: 300;"> |
| π§ LeetCode Daily Tracker |
| </h1> |
| <p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0; font-size: 16px;"> |
| Your coding journey companion |
| </p> |
| </div> |
| |
| <!-- Content --> |
| <div style="padding: 40px 30px;"> |
| <div style="background-color: {color['bg']}; border-left: 4px solid {color['primary']}; padding: 20px; margin-bottom: 25px; border-radius: 0 8px 8px 0;"> |
| {content} |
| {difficulty_badge} |
| </div> |
| |
| {problem_button} |
| |
| {tips_section} |
| |
| {motivation_section} |
| </div> |
| |
| <!-- Footer --> |
| <div style="background-color: #f8f9fa; padding: 25px 30px; border-top: 1px solid #eee;"> |
| <div style="text-align: center;"> |
| <p style="color: #666; margin: 0 0 15px 0; font-size: 14px;"> |
| Keep coding, keep growing! π± |
| </p> |
| <div style="margin: 15px 0;"> |
| <a href="https://leetcode.com" style="color: {color['primary']}; text-decoration: none; margin: 0 10px;">π LeetCode</a> |
| <span style="color: #ccc;">|</span> |
| <a href="https://github.com" style="color: {color['primary']}; text-decoration: none; margin: 0 10px;">π» GitHub</a> |
| <span style="color: #ccc;">|</span> |
| <a href="{unsubscribe_link if email_type != 'verification' else '#'}" style="color: #999; text-decoration: none; margin: 0 10px; font-size: 12px;">{'Unsubscribe' if email_type != 'verification' else ''}</a> |
| </div> |
| <p style="color: #999; font-size: 12px; margin: 15px 0 0 0;"> |
| Β© 2024 LeetCode Daily Tracker. Made with β€οΈ for coders. |
| </p> |
| </div> |
| </div> |
| </div> |
| </body> |
| </html> |
| """ |
|
|
|
|
| |
|
|
| EMAIL_REGEX = re.compile( |
| r"^[\w\.-]+@[\w\.-]+\.\w+$" |
| ) |
|
|
|
|
| def valid_email(email): |
| return bool(email and EMAIL_REGEX.match(email)) |
|
|
|
|
| def valid_leetcode(username): |
|
|
| if not username or len(username) < 3: |
| return False |
|
|
| try: |
| r = requests.get( |
| f"{LEETCODE_API}/{username}", |
| timeout=8 |
| ) |
| return r.status_code == 200 |
|
|
| except: |
| return False |
|
|
|
|
| |
|
|
| def get_daily_problem(): |
| try: |
| r = requests.get(f"{LEETCODE_API}/daily", timeout=10) |
| r.raise_for_status() |
|
|
| d = r.json() |
| print("π‘ Daily API response keys:", list(d.keys())) |
|
|
| |
| if "questionTitle" in d and "titleSlug" in d: |
| title = d["questionTitle"] |
| slug = d["titleSlug"] |
| link = d.get("questionLink", f"https://leetcode.com/problems/{slug}/") |
| difficulty = d.get("difficulty", "Unknown") |
| print(f"β
Found daily problem: {title} ({difficulty}) - {slug}") |
| return title, slug, link, difficulty |
|
|
| |
| if "title" in d and "titleSlug" in d: |
| title = d["title"] |
| slug = d["titleSlug"] |
| link = f"https://leetcode.com/problems/{slug}/" |
| difficulty = d.get("difficulty", "Unknown") |
| print(f"β
Found daily problem (fallback): {title} ({difficulty}) - {slug}") |
| return title, slug, link, difficulty |
|
|
| |
| print("β Available fields in API response:", list(d.keys())) |
| raise Exception("Could not find title and slug in daily API response") |
|
|
| except requests.exceptions.RequestException as e: |
| print(f"β Network error calling daily API: {e}") |
| raise Exception(f"Failed to fetch daily problem: {e}") |
| |
| except json.JSONDecodeError as e: |
| print(f"β JSON decode error: {e}") |
| raise Exception("Invalid JSON response from daily API") |
|
|
|
|
| def solved_today(username, slug): |
|
|
| try: |
| r = requests.get( |
| f"{LEETCODE_API}/{username}/acSubmission?limit=20", |
| timeout=10 |
| ) |
|
|
| if r.status_code != 200: |
| return False |
|
|
| d = r.json() |
|
|
| if "submission" in d: |
| subs = d["submission"] |
| elif "data" in d: |
| subs = d["data"] |
| elif isinstance(d, list): |
| subs = d |
| else: |
| return False |
|
|
| today = date.today() |
|
|
| for s in subs: |
|
|
| if not isinstance(s, dict): |
| continue |
|
|
| if s.get("titleSlug") != slug: |
| continue |
|
|
| ts = s.get("timestamp") |
|
|
| if not ts: |
| continue |
|
|
| solved = datetime.fromtimestamp( |
| int(ts), |
| tz=pytz.utc |
| ).date() |
|
|
| if solved == today: |
| return True |
|
|
| return False |
|
|
| except: |
| return False |
|
|
|
|
| |
|
|
| def subscribe(username, email, timezone): |
|
|
| if not valid_leetcode(username): |
| return "β Invalid LeetCode username" |
|
|
| if not valid_email(email): |
| return "β Invalid email" |
|
|
| conn = get_db() |
| cur = conn.cursor() |
|
|
| cur.execute(""" |
| SELECT email_verified, verification_token, unsubscribed |
| FROM users WHERE email=%s |
| """, (email,)) |
|
|
| row = cur.fetchone() |
|
|
| |
| if row: |
| verified, token, unsub = row |
|
|
| if verified and not unsub: |
| cur.close() |
| conn.close() |
| return "β οΈ Already subscribed" |
|
|
| if verified and unsub: |
| cur.execute(""" |
| UPDATE users |
| SET unsubscribed=false, |
| leetcode_username=%s, |
| timezone=%s, |
| last_sent_date=NULL, |
| last_sent_slot=NULL |
| WHERE email=%s |
| """, (username, timezone, email)) |
|
|
| conn.commit() |
| cur.close() |
| conn.close() |
| return "β
Re-subscribed" |
|
|
| |
| verification_content = f""" |
| <h2 style="color: #2196F3; margin: 0 0 15px 0;">Welcome back! π</h2> |
| <p style="color: #333; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;"> |
| We're excited to have you on your coding journey again! |
| </p> |
| <p style="color: #666; font-size: 14px; margin: 0;"> |
| Click the verification button above to activate your daily LeetCode reminders. |
| </p> |
| """ |
| |
| html_email = create_email_template( |
| "Email Verification", |
| verification_content, |
| f"{HF_URL}?verify={token}", |
| "verification" |
| ) |
|
|
| send_email(email, "π Please verify your email", html_email) |
| cur.close() |
| conn.close() |
| return "π© Verification re-sent" |
|
|
| |
| token = uuid.uuid4().hex |
|
|
| cur.execute(""" |
| INSERT INTO users( |
| leetcode_username,email,timezone, |
| email_verified,verification_token,unsubscribed |
| ) |
| VALUES(%s,%s,%s,false,%s,false) |
| """, (username, email, timezone, token)) |
|
|
| conn.commit() |
| cur.close() |
| conn.close() |
|
|
| |
| welcome_content = f""" |
| <h2 style="color: #4CAF50; margin: 0 0 15px 0;">Welcome to the club! π</h2> |
| <p style="color: #333; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;"> |
| You're about to embark on an amazing coding journey with daily LeetCode challenges! |
| </p> |
| <div style="background-color: white; border: 2px dashed #4CAF50; padding: 15px; border-radius: 8px; margin: 15px 0;"> |
| <p style="color: #4CAF50; font-weight: bold; margin: 0 0 5px 0;">π
Your Schedule:</p> |
| <p style="color: #666; font-size: 14px; margin: 0;"> |
| π
<strong>9 AM</strong> - Daily problem<br> |
| π <strong>3 PM</strong> - Gentle reminder<br> |
| π <strong>8 PM</strong> - Final reminder |
| </p> |
| </div> |
| <p style="color: #666; font-size: 14px; margin: 0;"> |
| Click the verification button above to start receiving your personalized reminders! |
| </p> |
| """ |
| |
| html_email = create_email_template( |
| "Welcome", |
| welcome_content, |
| f"{HF_URL}?verify={token}", |
| "verification" |
| ) |
|
|
| send_email(email, "π― Verify your LeetCode journey!", html_email) |
| return "π© Verification sent" |
|
|
|
|
| |
|
|
| def verify_user(token): |
|
|
| conn = get_db() |
| cur = conn.cursor() |
|
|
| cur.execute(""" |
| UPDATE users |
| SET email_verified=true |
| WHERE verification_token=%s |
| AND email_verified=false |
| """, (token,)) |
|
|
| ok = cur.rowcount |
|
|
| conn.commit() |
| cur.close() |
| conn.close() |
|
|
| if ok == 0: |
| return "β Invalid link" |
|
|
| return "β
Email verified" |
|
|
|
|
| |
|
|
| def unsubscribe_user(token): |
|
|
| conn = get_db() |
| cur = conn.cursor() |
|
|
| cur.execute(""" |
| UPDATE users |
| SET unsubscribed=true |
| WHERE verification_token=%s |
| """, (token,)) |
|
|
| ok = cur.rowcount |
|
|
| conn.commit() |
| cur.close() |
| conn.close() |
|
|
| if ok == 0: |
| return "β Invalid link" |
|
|
| return "β
Unsubscribed" |
|
|
|
|
| |
|
|
| def handle_url(request: gr.Request): |
|
|
| try: |
| params = request.query_params |
|
|
| if "verify" in params: |
| return verify_user(params["verify"]) |
|
|
| if "unsubscribe" in params: |
| return unsubscribe_user(params["unsubscribe"]) |
|
|
| return "" |
|
|
| except Exception as e: |
| print("URL error:", e) |
| return "" |
|
|
|
|
|
|
| def run_scheduler(secret): |
|
|
| if secret != CRON_SECRET: |
| return "β Unauthorized" |
|
|
| conn = get_db() |
| cur = conn.cursor() |
|
|
| cur.execute(""" |
| SELECT id,leetcode_username,email,timezone, |
| last_sent_date,last_sent_slot,verification_token |
| FROM users |
| WHERE email_verified=true |
| AND unsubscribed=false |
| """) |
|
|
| users = cur.fetchall() |
|
|
| try: |
| title, slug, problem_link, difficulty = get_daily_problem() |
| except Exception as e: |
| cur.close() |
| conn.close() |
| return f"β Failed to get daily problem: {e}" |
|
|
| now = datetime.now(pytz.utc) |
| sent = 0 |
|
|
| for uid, user, mail, tz, last_d, last_s, token in users: |
|
|
| try: |
| local = now.astimezone(pytz.timezone(tz)) |
| h = local.hour |
|
|
| |
| if 8 <= h <= 9: |
| slot = "morning" |
| subject = f"π
Today's LeetCode Challenge: {title}" |
| content = f""" |
| <h2 style="color: #4CAF50; margin: 0 0 15px 0;">Good morning, coder! βοΈ</h2> |
| <p style="color: #333; font-size: 18px; font-weight: bold; margin: 0 0 10px 0;"> |
| Today's Problem: <span style="color: #4CAF50;">{title}</span> |
| </p> |
| <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0;"> |
| Start your day with a fresh challenge! This {difficulty} problem is perfect for warming up |
| your coding muscles. Take your time to understand the requirements! π |
| </p> |
| """ |
| email_type = "morning" |
|
|
| elif 14 <= h <= 15: |
| slot = "afternoon" |
| subject = f"β° Afternoon Coding Break: {title}" |
| content = f""" |
| <h2 style="color: #FF9800; margin: 0 0 15px 0;">Time for a coding break! β‘</h2> |
| <p style="color: #333; font-size: 16px; margin: 0 0 15px 0;"> |
| Haven't tackled <strong style="color: #FF9800;">{title}</strong> yet? No worries! |
| </p> |
| <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0;"> |
| This {difficulty} problem is waiting for you. Sometimes a fresh afternoon perspective |
| can lead to breakthrough solutions! π‘ |
| </p> |
| """ |
| email_type = "afternoon" |
|
|
| elif 19 <= h <= 20: |
| slot = "night" |
| subject = f"π Last Call: {title}" |
| content = f""" |
| <h2 style="color: #F44336; margin: 0 0 15px 0;">Final reminder! π₯</h2> |
| <p style="color: #333; font-size: 16px; margin: 0 0 15px 0;"> |
| <strong style="color: #F44336;">{title}</strong> ({difficulty}) is still waiting for you! |
| </p> |
| <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;"> |
| Don't let the day end without giving it a try. Even reading through the problem |
| and thinking about approaches counts as progress! |
| </p> |
| <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 8px;"> |
| <p style="color: #856404; margin: 0; font-size: 14px;"> |
| πͺ <strong>Remember:</strong> Consistency beats perfection. Every attempt makes you stronger! |
| </p> |
| </div> |
| """ |
| email_type = "night" |
|
|
| else: |
| continue |
|
|
| today = date.today() |
|
|
| |
| if last_d == today and last_s == slot: |
| print(f"βοΈ Skipping {mail} - already sent {slot} email today") |
| continue |
|
|
| |
| if solved_today(user, slug): |
| print(f"β
{user} already solved {slug} - skipping email") |
| continue |
|
|
| |
| html_email = create_email_template( |
| title, |
| content, |
| f"{HF_URL}?unsubscribe={token}", |
| email_type, |
| problem_link, |
| difficulty |
| ) |
|
|
| ok = send_email(mail, subject, html_email) |
|
|
| if not ok: |
| print(f"β Failed to send email to {mail}") |
| continue |
|
|
| |
| cur.execute(""" |
| UPDATE users |
| SET last_sent_date=%s, |
| last_sent_slot=%s |
| WHERE id=%s |
| """, (today, slot, uid)) |
|
|
| sent += 1 |
|
|
| except Exception as e: |
| print(f"β Error processing user {user} ({mail}): {e}") |
| continue |
|
|
| conn.commit() |
| cur.close() |
| conn.close() |
|
|
| return f"β
Scheduler completed. Sent: {sent} emails" |
|
|
|
|
|
|
| |
| with gr.Blocks( |
| title="LeetCode Notifier", |
| theme=gr.themes.Soft(), |
| css=""" |
| .gradio-container { |
| max-width: 800px !important; |
| margin: auto !important; |
| } |
| """ |
| ) as app: |
|
|
| gr.Markdown(""" |
| # π¬ LeetCode Daily Email Notifier |
| |
| Get personalized daily LeetCode problem reminders sent directly to your inbox! |
| Never miss a day of coding practice. |
| """) |
|
|
| with gr.Row(): |
| with gr.Column(): |
| u = gr.Textbox( |
| label="π§βπ» LeetCode Username", |
| placeholder="Enter your LeetCode username", |
| info="We'll verify this username exists on LeetCode" |
| ) |
| m = gr.Textbox( |
| label="π§ Email Address", |
| placeholder="your.email@gmail.com", |
| info="You'll receive a verification email" |
| ) |
| tz = gr.Dropdown( |
| choices=sorted(pytz.all_timezones), |
| value="Asia/Kolkata", |
| label="π Timezone", |
| info="Choose your timezone for proper scheduling" |
| ) |
|
|
| with gr.Row(): |
| subscribe_btn = gr.Button("π Subscribe", variant="primary", scale=2) |
| |
| out = gr.Textbox(label="π Status", interactive=False, lines=2) |
| |
| subscribe_btn.click(subscribe, [u, m, tz], out) |
|
|
| gr.Markdown(""" |
| --- |
| ### β° Email Schedule |
| - **π
9:00 AM** - Daily problem notification |
| - **π 3:00 PM** - Gentle reminder (if not solved) |
| - **π 8:00 PM** - Final reminder (if not solved) |
| |
| ### π Admin Panel |
| """) |
|
|
| with gr.Row(): |
| sec = gr.Textbox( |
| label="π Secret Key", |
| type="password", |
| placeholder="Enter scheduler secret key" |
| ) |
| run_btn = gr.Button("βΆοΈ Run Scheduler", variant="secondary") |
|
|
| run_btn.click(run_scheduler, sec, out) |
|
|
| |
| app.load(handle_url, outputs=out) |
|
|
|
|
| if __name__ == "__main__": |
| app.launch(debug=True) |