Pranesh64/Leetcode-daily-problem-tracker
0
1import os2import re3import uuid4import pytz5import psycopg26import requests7import gradio as gr8import json9import pickle10import base6411 12from google.auth.transport.requests import Request13from googleapiclient.discovery import build14from google.oauth2.credentials import Credentials15 16from email.mime.text import MIMEText17from datetime import datetime, date18from dotenv import load_dotenv19 20 21# ================= LOAD ENV =================22 23load_dotenv()24 25DB_URL = os.getenv("DB_URL")26GMAIL_USER = os.getenv("GMAIL_USER")27CRON_SECRET = os.getenv("CRON")28HF_URL = os.getenv("HF_URL")29 30LEETCODE_API = "https://leetcode-api-vercel.vercel.app"31 32SCOPES = ["https://www.googleapis.com/auth/gmail.send"]33TOKEN_FILE = "token.pkl"34 35 36# ================= DB =================37 38def get_db():39 return psycopg2.connect(DB_URL, sslmode="require")40 41 42# ================= GMAIL =================43 44def get_gmail_service():45 46 creds = None47 48 if os.path.exists(TOKEN_FILE):49 try:50 with open(TOKEN_FILE, "rb") as f:51 creds = pickle.load(f)52 except:53 creds = None54 55 if creds and creds.expired and creds.refresh_token:56 try:57 creds.refresh(Request())58 59 with open(TOKEN_FILE, "wb") as f:60 pickle.dump(creds, f)61 62 except:63 creds = None64 65 if not creds:66 raise Exception("❌ token.pkl missing. Upload valid token.")67 68 return build("gmail", "v1", credentials=creds)69 70 71def send_email(to, subject, html):72 73 try:74 service = get_gmail_service()75 76 msg = MIMEText(html, "html")77 msg["To"] = to78 msg["From"] = GMAIL_USER79 msg["Subject"] = subject80 81 raw = base64.urlsafe_b64encode(82 msg.as_bytes()83 ).decode()84 85 body = {"raw": raw}86 87 service.users().messages().send(88 userId="me",89 body=body90 ).execute()91 92 print("✅ Sent:", to)93 return True94 95 except Exception as e:96 print("❌ Gmail error:", e)97 return False98 99 100def create_email_template(title, content, unsubscribe_link, email_type="reminder", problem_link=None, difficulty=None):101 """Create a beautiful HTML email template"""102 103 # Color scheme based on email type104 colors = {105 "morning": {"primary": "#4CAF50", "secondary": "#81C784", "bg": "#E8F5E8"},106 "afternoon": {"primary": "#FF9800", "secondary": "#FFB74D", "bg": "#FFF3E0"},107 "night": {"primary": "#F44336", "secondary": "#EF5350", "bg": "#FFEBEE"},108 "verification": {"primary": "#2196F3", "secondary": "#64B5F6", "bg": "#E3F2FD"}109 }110 111 color = colors.get(email_type, colors["morning"])112 113 # Difficulty badge colors114 difficulty_colors = {115 "Easy": "#00B04F",116 "Medium": "#FFA116", 117 "Hard": "#FF375F"118 }119 120 # Handle special cases for verification emails121 if email_type == "verification":122 problem_button = f"""123 <div style="text-align: center; margin: 30px 0;">124 <a href="{unsubscribe_link}" 125 style="display: inline-block; background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); 126 color: white; padding: 15px 30px; text-decoration: none; border-radius: 25px; 127 font-weight: bold; font-size: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); 128 transition: transform 0.2s;">129 ✅ Verify Email130 </a>131 </div>132 """133 tips_section = ""134 motivation_section = ""135 difficulty_badge = ""136 else:137 # Use provided problem_link or construct from title138 if problem_link:139 link_url = problem_link140 else:141 link_url = f"https://leetcode.com/problems/{title.lower().replace(' ', '-').replace('.', '')}"142 143 # Add difficulty badge144 if difficulty:145 diff_color = difficulty_colors.get(difficulty, "#666")146 difficulty_badge = f"""147 <div style="text-align: center; margin: 10px 0;">148 <span style="background-color: {diff_color}; color: white; padding: 4px 12px; 149 border-radius: 12px; font-size: 12px; font-weight: bold;">150 {difficulty}151 </span>152 </div>153 """154 else:155 difficulty_badge = ""156 157 problem_button = f"""158 <div style="text-align: center; margin: 30px 0;">159 <a href="{link_url}" 160 style="display: inline-block; background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); 161 color: white; padding: 15px 30px; text-decoration: none; border-radius: 25px; 162 font-weight: bold; font-size: 16px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); 163 transition: transform 0.2s;">164 🚀 Solve Problem165 </a>166 </div>167 """168 169 # Dynamic tips based on difficulty170 if difficulty == "Easy":171 tips_content = """172 <li>Focus on understanding the problem clearly</li>173 <li>Think about the simplest approach first</li>174 <li>Test with the given examples</li>175 <li>Consider edge cases like empty inputs</li>176 """177 elif difficulty == "Medium":178 tips_content = """179 <li>Break the problem into smaller subproblems</li>180 <li>Consider multiple approaches (greedy, DP, etc.)</li>181 <li>Think about time and space complexity</li>182 <li>Use appropriate data structures</li>183 """184 else: # Hard185 tips_content = """186 <li>Study similar problems and patterns</li>187 <li>Don't rush - take time to understand</li>188 <li>Consider advanced algorithms and techniques</li>189 <li>Break it down step by step</li>190 """191 192 tips_section = f"""193 <!-- Tips Section -->194 <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0;">195 <h3 style="color: #333; margin: 0 0 15px 0; font-size: 18px;">💡 {difficulty} Problem Tips</h3>196 <ul style="color: #666; margin: 0; padding-left: 20px; line-height: 1.6;">197 {tips_content}198 </ul>199 </div>200 """201 202 motivation_quotes = [203 "The expert in anything was once a beginner.",204 "Every problem is a step forward in your journey.",205 "Consistency beats perfection every time.",206 "Code today, conquer tomorrow.",207 "Small progress is still progress."208 ]209 import random210 quote = random.choice(motivation_quotes)211 212 motivation_section = f"""213 <!-- Stats or Motivation -->214 <div style="text-align: center; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; margin: 25px 0;">215 <p style="color: white; margin: 0; font-size: 16px; font-style: italic;">216 "{quote}" 💪217 </p>218 </div>219 """220 221 return f"""222 <!DOCTYPE html>223 <html>224 <head>225 <meta charset="UTF-8">226 <meta name="viewport" content="width=device-width, initial-scale=1.0">227 <title>LeetCode Daily Tracker</title>228 </head>229 <body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f5f5f5;">230 <div style="max-width: 600px; margin: 0 auto; background-color: white; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">231 232 <!-- Header -->233 <div style="background: linear-gradient(135deg, {color['primary']}, {color['secondary']}); padding: 30px; text-align: center;">234 <h1 style="color: white; margin: 0; font-size: 28px; font-weight: 300;">235 🧠 LeetCode Daily Tracker236 </h1>237 <p style="color: rgba(255,255,255,0.9); margin: 10px 0 0 0; font-size: 16px;">238 Your coding journey companion239 </p>240 </div>241 242 <!-- Content -->243 <div style="padding: 40px 30px;">244 <div style="background-color: {color['bg']}; border-left: 4px solid {color['primary']}; padding: 20px; margin-bottom: 25px; border-radius: 0 8px 8px 0;">245 {content}246 {difficulty_badge}247 </div>248 249 {problem_button}250 251 {tips_section}252 253 {motivation_section}254 </div>255 256 <!-- Footer -->257 <div style="background-color: #f8f9fa; padding: 25px 30px; border-top: 1px solid #eee;">258 <div style="text-align: center;">259 <p style="color: #666; margin: 0 0 15px 0; font-size: 14px;">260 Keep coding, keep growing! 🌱261 </p>262 <div style="margin: 15px 0;">263 <a href="https://leetcode.com" style="color: {color['primary']}; text-decoration: none; margin: 0 10px;">📊 LeetCode</a>264 <span style="color: #ccc;">|</span>265 <a href="https://github.com" style="color: {color['primary']}; text-decoration: none; margin: 0 10px;">💻 GitHub</a>266 <span style="color: #ccc;">|</span>267 <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>268 </div>269 <p style="color: #999; font-size: 12px; margin: 15px 0 0 0;">270 © 2024 LeetCode Daily Tracker. Made with ❤️ for coders.271 </p>272 </div>273 </div>274 </div>275 </body>276 </html>277 """278 279 280# ================= VALIDATION =================281 282EMAIL_REGEX = re.compile(283 r"^[\w\.-]+@[\w\.-]+\.\w+$"284)285 286 287def valid_email(email):288 return bool(email and EMAIL_REGEX.match(email))289 290 291def valid_leetcode(username):292 293 if not username or len(username) < 3:294 return False295 296 try:297 r = requests.get(298 f"{LEETCODE_API}/{username}",299 timeout=8300 )301 return r.status_code == 200302 303 except:304 return False305 306 307# ================= LEETCODE =================308 309def get_daily_problem():310 try:311 r = requests.get(f"{LEETCODE_API}/daily", timeout=10)312 r.raise_for_status()313 314 d = r.json()315 print("📡 Daily API response keys:", list(d.keys()))316 317 # Handle the current API format318 if "questionTitle" in d and "titleSlug" in d:319 title = d["questionTitle"]320 slug = d["titleSlug"]321 link = d.get("questionLink", f"https://leetcode.com/problems/{slug}/")322 difficulty = d.get("difficulty", "Unknown")323 print(f"✅ Found daily problem: {title} ({difficulty}) - {slug}")324 return title, slug, link, difficulty325 326 # Fallback for older format327 if "title" in d and "titleSlug" in d:328 title = d["title"] 329 slug = d["titleSlug"]330 link = f"https://leetcode.com/problems/{slug}/"331 difficulty = d.get("difficulty", "Unknown")332 print(f"✅ Found daily problem (fallback): {title} ({difficulty}) - {slug}")333 return title, slug, link, difficulty334 335 # If we can't find the expected fields, print the response336 print("❌ Available fields in API response:", list(d.keys()))337 raise Exception("Could not find title and slug in daily API response")338 339 except requests.exceptions.RequestException as e:340 print(f"❌ Network error calling daily API: {e}")341 raise Exception(f"Failed to fetch daily problem: {e}")342 343 except json.JSONDecodeError as e:344 print(f"❌ JSON decode error: {e}")345 raise Exception("Invalid JSON response from daily API")346 347 348def solved_today(username, slug):349 350 try:351 r = requests.get(352 f"{LEETCODE_API}/{username}/acSubmission?limit=20",353 timeout=10354 )355 356 if r.status_code != 200:357 return False358 359 d = r.json()360 361 if "submission" in d:362 subs = d["submission"]363 elif "data" in d:364 subs = d["data"]365 elif isinstance(d, list):366 subs = d367 else:368 return False369 370 today = date.today()371 372 for s in subs:373 374 if not isinstance(s, dict):375 continue376 377 if s.get("titleSlug") != slug:378 continue379 380 ts = s.get("timestamp")381 382 if not ts:383 continue384 385 solved = datetime.fromtimestamp(386 int(ts),387 tz=pytz.utc388 ).date()389 390 if solved == today:391 return True392 393 return False394 395 except:396 return False397 398 399# ================= SUBSCRIBE =================400 401def subscribe(username, email, timezone):402 403 if not valid_leetcode(username):404 return "❌ Invalid LeetCode username"405 406 if not valid_email(email):407 return "❌ Invalid email"408 409 conn = get_db()410 cur = conn.cursor()411 412 cur.execute("""413 SELECT email_verified, verification_token, unsubscribed414 FROM users WHERE email=%s415 """, (email,))416 417 row = cur.fetchone()418 419 # Existing user logic...420 if row:421 verified, token, unsub = row422 423 if verified and not unsub:424 cur.close()425 conn.close()426 return "⚠️ Already subscribed"427 428 if verified and unsub:429 cur.execute("""430 UPDATE users431 SET unsubscribed=false,432 leetcode_username=%s,433 timezone=%s,434 last_sent_date=NULL,435 last_sent_slot=NULL436 WHERE email=%s437 """, (username, timezone, email))438 439 conn.commit()440 cur.close()441 conn.close()442 return "✅ Re-subscribed"443 444 # Resend verification with enhanced template445 verification_content = f"""446 <h2 style="color: #2196F3; margin: 0 0 15px 0;">Welcome back! 👋</h2>447 <p style="color: #333; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;">448 We're excited to have you on your coding journey again! 449 </p>450 <p style="color: #666; font-size: 14px; margin: 0;">451 Click the verification button above to activate your daily LeetCode reminders.452 </p>453 """454 455 html_email = create_email_template(456 "Email Verification", 457 verification_content, 458 f"{HF_URL}?verify={token}", 459 "verification"460 )461 462 send_email(email, "🔔 Please verify your email", html_email)463 cur.close()464 conn.close()465 return "📩 Verification re-sent"466 467 # New user - remove duplicate code468 token = uuid.uuid4().hex469 470 cur.execute("""471 INSERT INTO users(472 leetcode_username,email,timezone,473 email_verified,verification_token,unsubscribed474 )475 VALUES(%s,%s,%s,false,%s,false)476 """, (username, email, timezone, token))477 478 conn.commit()479 cur.close()480 conn.close()481 482 # Welcome email with enhanced template483 welcome_content = f"""484 <h2 style="color: #4CAF50; margin: 0 0 15px 0;">Welcome to the club! 🎉</h2>485 <p style="color: #333; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;">486 You're about to embark on an amazing coding journey with daily LeetCode challenges!487 </p>488 <div style="background-color: white; border: 2px dashed #4CAF50; padding: 15px; border-radius: 8px; margin: 15px 0;">489 <p style="color: #4CAF50; font-weight: bold; margin: 0 0 5px 0;">📅 Your Schedule:</p>490 <p style="color: #666; font-size: 14px; margin: 0;">491 🌅 <strong>9 AM</strong> - Daily problem<br>492 🌆 <strong>3 PM</strong> - Gentle reminder<br>493 🌙 <strong>8 PM</strong> - Final reminder494 </p>495 </div>496 <p style="color: #666; font-size: 14px; margin: 0;">497 Click the verification button above to start receiving your personalized reminders!498 </p>499 """500 501 html_email = create_email_template(502 "Welcome", 503 welcome_content, 504 f"{HF_URL}?verify={token}", 505 "verification"506 )507 508 send_email(email, "🎯 Verify your LeetCode journey!", html_email)509 return "📩 Verification sent"510 511 512# ================= VERIFY =================513 514def verify_user(token):515 516 conn = get_db()517 cur = conn.cursor()518 519 cur.execute("""520 UPDATE users521 SET email_verified=true522 WHERE verification_token=%s523 AND email_verified=false524 """, (token,))525 526 ok = cur.rowcount527 528 conn.commit()529 cur.close()530 conn.close()531 532 if ok == 0:533 return "❌ Invalid link"534 535 return "✅ Email verified"536 537 538# ================= UNSUBSCRIBE =================539 540def unsubscribe_user(token):541 542 conn = get_db()543 cur = conn.cursor()544 545 cur.execute("""546 UPDATE users547 SET unsubscribed=true548 WHERE verification_token=%s549 """, (token,))550 551 ok = cur.rowcount552 553 conn.commit()554 cur.close()555 conn.close()556 557 if ok == 0:558 return "❌ Invalid link"559 560 return "✅ Unsubscribed"561 562 563# ================= URL HANDLER =================564 565def handle_url(request: gr.Request):566 567 try:568 params = request.query_params569 570 if "verify" in params:571 return verify_user(params["verify"])572 573 if "unsubscribe" in params:574 return unsubscribe_user(params["unsubscribe"])575 576 return ""577 578 except Exception as e:579 print("URL error:", e)580 return ""581 582 583 584def run_scheduler(secret):585 586 if secret != CRON_SECRET:587 return "❌ Unauthorized"588 589 conn = get_db()590 cur = conn.cursor()591 592 cur.execute("""593 SELECT id,leetcode_username,email,timezone,594 last_sent_date,last_sent_slot,verification_token595 FROM users596 WHERE email_verified=true597 AND unsubscribed=false598 """)599 600 users = cur.fetchall()601 602 try:603 title, slug, problem_link, difficulty = get_daily_problem()604 except Exception as e:605 cur.close()606 conn.close()607 return f"❌ Failed to get daily problem: {e}"608 609 now = datetime.now(pytz.utc)610 sent = 0611 612 for uid, user, mail, tz, last_d, last_s, token in users:613 614 try:615 local = now.astimezone(pytz.timezone(tz))616 h = local.hour617 618 # Enhanced email content based on time619 if 8 <= h <= 9:620 slot = "morning"621 subject = f"🌅 Today's LeetCode Challenge: {title}"622 content = f"""623 <h2 style="color: #4CAF50; margin: 0 0 15px 0;">Good morning, coder! ☀️</h2>624 <p style="color: #333; font-size: 18px; font-weight: bold; margin: 0 0 10px 0;">625 Today's Problem: <span style="color: #4CAF50;">{title}</span>626 </p>627 <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0;">628 Start your day with a fresh challenge! This {difficulty} problem is perfect for warming up 629 your coding muscles. Take your time to understand the requirements! 🚀630 </p>631 """632 email_type = "morning"633 634 elif 14 <= h <= 15:635 slot = "afternoon" 636 subject = f"⏰ Afternoon Coding Break: {title}"637 content = f"""638 <h2 style="color: #FF9800; margin: 0 0 15px 0;">Time for a coding break! ⚡</h2>639 <p style="color: #333; font-size: 16px; margin: 0 0 15px 0;">640 Haven't tackled <strong style="color: #FF9800;">{title}</strong> yet? No worries!641 </p>642 <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0;">643 This {difficulty} problem is waiting for you. Sometimes a fresh afternoon perspective 644 can lead to breakthrough solutions! 💡645 </p>646 """647 email_type = "afternoon"648 649 elif 19 <= h <= 20:650 slot = "night"651 subject = f"🌙 Last Call: {title}"652 content = f"""653 <h2 style="color: #F44336; margin: 0 0 15px 0;">Final reminder! 🔥</h2>654 <p style="color: #333; font-size: 16px; margin: 0 0 15px 0;">655 <strong style="color: #F44336;">{title}</strong> ({difficulty}) is still waiting for you!656 </p>657 <p style="color: #666; font-size: 16px; line-height: 1.6; margin: 0 0 15px 0;">658 Don't let the day end without giving it a try. Even reading through the problem 659 and thinking about approaches counts as progress! 660 </p>661 <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 8px;">662 <p style="color: #856404; margin: 0; font-size: 14px;">663 💪 <strong>Remember:</strong> Consistency beats perfection. Every attempt makes you stronger!664 </p>665 </div>666 """667 email_type = "night"668 669 else:670 continue671 672 today = date.today()673 674 # Check if already sent today675 if last_d == today and last_s == slot:676 print(f"⏭️ Skipping {mail} - already sent {slot} email today")677 continue678 679 # Check if user already solved the problem680 if solved_today(user, slug):681 print(f"✅ {user} already solved {slug} - skipping email")682 continue683 684 # Create beautiful HTML email with all enhancements685 html_email = create_email_template(686 title, 687 content, 688 f"{HF_URL}?unsubscribe={token}", 689 email_type,690 problem_link,691 difficulty692 )693 694 ok = send_email(mail, subject, html_email)695 696 if not ok:697 print(f"❌ Failed to send email to {mail}")698 continue699 700 # Update database701 cur.execute("""702 UPDATE users703 SET last_sent_date=%s,704 last_sent_slot=%s705 WHERE id=%s706 """, (today, slot, uid))707 708 sent += 1709 710 except Exception as e:711 print(f"❌ Error processing user {user} ({mail}): {e}")712 continue713 714 conn.commit()715 cur.close()716 conn.close()717 718 return f"✅ Scheduler completed. Sent: {sent} emails"719 720 721 722# ================= UI =================723with gr.Blocks(724 title="LeetCode Notifier",725 theme=gr.themes.Soft(),726 css="""727 .gradio-container {728 max-width: 800px !important;729 margin: auto !important;730 }731 """732) as app:733 734 gr.Markdown("""735 # 📬 LeetCode Daily Email Notifier736 737 Get personalized daily LeetCode problem reminders sent directly to your inbox! 738 Never miss a day of coding practice.739 """)740 741 with gr.Row():742 with gr.Column():743 u = gr.Textbox(744 label="🧑💻 LeetCode Username", 745 placeholder="Enter your LeetCode username",746 info="We'll verify this username exists on LeetCode"747 )748 m = gr.Textbox(749 label="📧 Email Address", 750 placeholder="your.email@gmail.com",751 info="You'll receive a verification email"752 )753 tz = gr.Dropdown(754 choices=sorted(pytz.all_timezones), 755 value="Asia/Kolkata", 756 label="🌍 Timezone",757 info="Choose your timezone for proper scheduling"758 )759 760 with gr.Row():761 subscribe_btn = gr.Button("🚀 Subscribe", variant="primary", scale=2)762 763 out = gr.Textbox(label="📝 Status", interactive=False, lines=2)764 765 subscribe_btn.click(subscribe, [u, m, tz], out)766 767 gr.Markdown("""768 ---769 ### ⏰ Email Schedule770 - **🌅 9:00 AM** - Daily problem notification771 - **🌆 3:00 PM** - Gentle reminder (if not solved)772 - **🌙 8:00 PM** - Final reminder (if not solved)773 774 ### 🔒 Admin Panel775 """)776 777 with gr.Row():778 sec = gr.Textbox(779 label="🔑 Secret Key", 780 type="password", 781 placeholder="Enter scheduler secret key"782 )783 run_btn = gr.Button("▶️ Run Scheduler", variant="secondary")784 785 run_btn.click(run_scheduler, sec, out)786 787 # URL handler for verification/unsubscribe788 app.load(handle_url, outputs=out)789 790 791if __name__ == "__main__":792 app.launch(debug=True)