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 ENV ================= 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" # ================= DB ================= def get_db(): return psycopg2.connect(DB_URL, sslmode="require") # ================= GMAIL ================= def get_gmail_service(): creds = None # First, try to load existing token if os.path.exists(TOKEN_FILE): try: with open(TOKEN_FILE, "rb") as f: creds = pickle.load(f) except Exception as e: print(f"â ī¸ Failed to load token.pkl: {e}") creds = None # Refresh token if needed if creds and creds.expired and creds.refresh_token: try: print("đ Refreshing expired token...") creds.refresh(Request()) # Save refreshed token with open(TOKEN_FILE, "wb") as f: pickle.dump(creds, f) print("â Token refreshed and saved") except Exception as e: print(f"â Failed to refresh token: {e}") creds = None # If no valid token, try to create from credentials.json if not creds or not creds.valid: creds = create_token_from_credentials() return build("gmail", "v1", credentials=creds) def create_token_from_credentials(): """Create token.pkl from credentials.json using OAuth flow""" credentials_file = "credentials.json" if not os.path.exists(credentials_file): raise Exception(f""" â Neither {TOKEN_FILE} nor {credentials_file} found! Please provide one of: 1. Upload token.pkl (ready-to-use token) 2. Upload credentials.json (will generate token.pkl) To get credentials.json: 1. Go to Google Cloud Console 2. Enable Gmail API 3. Create OAuth 2.0 credentials 4. Download as credentials.json """) try: print(f"đ Found {credentials_file}, creating token...") from google_auth_oauthlib.flow import InstalledAppFlow # Create flow from credentials flow = InstalledAppFlow.from_client_secrets_file( credentials_file, SCOPES ) # Run local server for OAuth (will open browser) creds = flow.run_local_server( port=8080, prompt='consent', authorization_prompt_message='Please visit this URL to authorize: {url}', success_message='Authorization complete! You can close this window.', open_browser=True ) # Save token for future use with open(TOKEN_FILE, "wb") as f: pickle.dump(creds, f) print(f"â Token created and saved to {TOKEN_FILE}") print("âšī¸ You can now delete credentials.json for security") return creds except ImportError: raise Exception(""" â Missing required package! Please install: pip install google-auth-oauthlib This is needed to create token from credentials.json """) except Exception as e: raise Exception(f""" â Failed to create token from credentials.json: {e} Common issues: 1. Invalid credentials.json format 2. Gmail API not enabled 3. OAuth consent screen not configured 4. Port 8080 already in use Please check your Google Cloud Console settings. """) def validate_gmail_setup(): """Validate Gmail API setup and provide helpful error messages""" try: service = get_gmail_service() # Test API access profile = service.users().getProfile(userId="me").execute() email = profile.get('emailAddress') print(f"â Gmail API connected: {email}") return True, email except Exception as e: error_msg = str(e) if "credentials" in error_msg.lower(): return False, "â Missing credentials. Upload token.pkl or credentials.json" elif "quota" in error_msg.lower(): return False, "â Gmail API quota exceeded. Try again later" elif "permission" in error_msg.lower(): return False, "â Insufficient permissions. Re-authorize with full Gmail access" else: return False, f"â Gmail API error: {error_msg}" 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""" # Color scheme based on email type 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 badge colors difficulty_colors = { "Easy": "#00B04F", "Medium": "#FFA116", "Hard": "#FF375F" } # Handle special cases for verification emails if email_type == "verification": problem_button = f"""
""" tips_section = "" motivation_section = "" difficulty_badge = "" else: # Use provided problem_link or construct from title if problem_link: link_url = problem_link else: link_url = f"https://leetcode.com/problems/{title.lower().replace(' ', '-').replace('.', '')}" # Add difficulty badge if difficulty: diff_color = difficulty_colors.get(difficulty, "#666") difficulty_badge = f""""{quote}" đĒ
Your coding journey companion
Keep coding, keep growing! đą
Š 2024 LeetCode Daily Tracker. Made with â¤ī¸ for coders.
We're excited to have you on your coding journey again!
Click the verification button above to activate your daily LeetCode reminders.
""" 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" # New user - remove duplicate code 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 email with enhanced template welcome_content = f"""You're about to embark on an amazing coding journey with daily LeetCode challenges!
đ Your Schedule:
đ
9 AM - Daily problem
đ 3 PM - Gentle reminder
đ 8 PM - Final reminder
Click the verification button above to start receiving your personalized reminders!
""" 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" # ================= VERIFY ================= 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" # ================= UNSUBSCRIBE ================= 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" # ================= URL HANDLER ================= 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 # Enhanced email content based on time if 8 <= h <= 9: slot = "morning" subject = f"đ Today's LeetCode Challenge: {title}" content = f"""Today's Problem: {title}
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! đ
""" email_type = "morning" elif 14 <= h <= 15: slot = "afternoon" subject = f"â° Afternoon Coding Break: {title}" content = f"""Haven't tackled {title} yet? No worries!
This {difficulty} problem is waiting for you. Sometimes a fresh afternoon perspective can lead to breakthrough solutions! đĄ
""" email_type = "afternoon" elif 19 <= h <= 20: slot = "night" subject = f"đ Last Call: {title}" content = f"""{title} ({difficulty}) is still waiting for you!
Don't let the day end without giving it a try. Even reading through the problem and thinking about approaches counts as progress!
đĒ Remember: Consistency beats perfection. Every attempt makes you stronger!