CoolFace
Apppublic

Pranesh64/Leetcode-daily-problem-tracker

sourceHugging Faceupdated 7mo agoView on Hugging Face
0likes
app.py792 linesDownload Raw Back to root
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)