Pranesh64 commited on
Commit
b497eb4
Β·
verified Β·
1 Parent(s): f1dfe92

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +446 -0
app.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import uuid
4
+ import pytz
5
+ import smtplib
6
+ import psycopg2
7
+ import requests
8
+ import gradio as gr
9
+
10
+ from email.mime.text import MIMEText
11
+ from email.mime.multipart import MIMEMultipart
12
+ from datetime import datetime, date
13
+ from dotenv import load_dotenv
14
+ from urllib.parse import parse_qs
15
+
16
+
17
+ # ================= LOAD ENV =================
18
+
19
+ load_dotenv()
20
+
21
+ DB_URL = os.getenv("DB_URL")
22
+ GMAIL_USER = os.getenv("GMAIL_USER")
23
+ GMAIL_APP_PASSWORD = os.getenv("GMAIL_APP_PASSWORD")
24
+ CRON_SECRET = os.getenv("CRON")
25
+ HF_URL = os.getenv("HF_URL")
26
+
27
+ LEETCODE_API = "https://leetcode-api-vercel.vercel.app"
28
+
29
+
30
+ # ================= DB =================
31
+
32
+ def get_db():
33
+ return psycopg2.connect(DB_URL, sslmode="require")
34
+
35
+
36
+ # ================= EMAIL =================
37
+
38
+ def send_email(to, subject, html):
39
+
40
+ msg = MIMEMultipart("alternative")
41
+ msg["From"] = f"LeetCode Tracker <{GMAIL_USER}>"
42
+ msg["To"] = to
43
+ msg["Subject"] = subject
44
+
45
+ msg.attach(MIMEText(html, "html"))
46
+
47
+ try:
48
+ with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
49
+ server.login(GMAIL_USER, GMAIL_APP_PASSWORD)
50
+ server.sendmail(msg["From"], to, msg.as_string())
51
+
52
+ print("βœ… Email sent:", to)
53
+ return True
54
+
55
+ except Exception as e:
56
+ print("❌ Email error:", e)
57
+ return False
58
+
59
+
60
+ # ================= VALIDATION =================
61
+
62
+ EMAIL_REGEX = re.compile(
63
+ r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
64
+ )
65
+
66
+
67
+ def valid_email(email):
68
+ return bool(email and EMAIL_REGEX.match(email))
69
+
70
+
71
+ def valid_leetcode(username):
72
+
73
+ if not username or len(username) < 3:
74
+ return False
75
+
76
+ try:
77
+ r = requests.get(
78
+ f"{LEETCODE_API}/{username}",
79
+ timeout=8
80
+ )
81
+
82
+ if r.status_code != 200:
83
+ return False
84
+
85
+ data = r.json()
86
+
87
+ return "username" in data or "profile" in data
88
+
89
+ except Exception:
90
+ return False
91
+
92
+
93
+ # ================= LEETCODE =================
94
+
95
+ def get_daily_problem():
96
+
97
+ r = requests.get(f"{LEETCODE_API}/daily", timeout=10)
98
+ r.raise_for_status()
99
+
100
+ data = r.json()
101
+
102
+ if "questionTitle" in data:
103
+ return data["questionTitle"], data["titleSlug"]
104
+
105
+ if "title" in data:
106
+ return data["title"], data["titleSlug"]
107
+
108
+ raise ValueError("Invalid daily API format")
109
+
110
+
111
+ def solved_today(username, slug):
112
+
113
+ try:
114
+ r = requests.get(
115
+ f"{LEETCODE_API}/{username}/acSubmission?limit=20",
116
+ timeout=10,
117
+ )
118
+
119
+ if r.status_code != 200:
120
+ return False
121
+
122
+ data = r.json()
123
+
124
+ # βœ… New API format (your case)
125
+ if isinstance(data, dict) and "submission" in data:
126
+ submissions = data["submission"]
127
+
128
+ # βœ… Old API format
129
+ elif isinstance(data, dict) and "data" in data:
130
+ submissions = data["data"]
131
+
132
+ # βœ… Direct list format
133
+ elif isinstance(data, list):
134
+ submissions = data
135
+
136
+ # ❌ Error / invalid format
137
+ else:
138
+ print("⚠️ Unknown submission format:", data)
139
+ return False
140
+
141
+ # Get today's timestamp (Unix day start)
142
+ today = date.today()
143
+
144
+ for s in submissions:
145
+
146
+ if not isinstance(s, dict):
147
+ continue
148
+
149
+ if s.get("titleSlug") != slug:
150
+ continue
151
+
152
+ ts = s.get("timestamp")
153
+
154
+ if not ts:
155
+ continue
156
+
157
+ # Convert Unix timestamp β†’ date
158
+ solved_date = datetime.fromtimestamp(
159
+ int(ts),
160
+ tz=pytz.utc
161
+ ).date()
162
+
163
+ if solved_date == today:
164
+ return True
165
+
166
+ return False
167
+
168
+ except Exception as e:
169
+ print("⚠️ solved_today error:", e)
170
+ return False
171
+
172
+
173
+ # ================= SUBSCRIBE =================
174
+
175
+ def subscribe(username, email, timezone):
176
+
177
+ if not valid_leetcode(username):
178
+ return "❌ Invalid LeetCode username"
179
+
180
+ if not valid_email(email):
181
+ return "❌ Invalid email format"
182
+
183
+ conn = get_db()
184
+ cur = conn.cursor()
185
+
186
+ # ---- Duplicate / Rate limit ----
187
+ cur.execute("""
188
+ SELECT email_verified, verification_token
189
+ FROM users
190
+ WHERE email = %s
191
+ """, (email,))
192
+
193
+ row = cur.fetchone()
194
+
195
+ # ---- Existing user ----
196
+ if row:
197
+
198
+ verified, token = row
199
+
200
+ if verified:
201
+ cur.close()
202
+ conn.close()
203
+ return "⚠️ Already subscribed"
204
+
205
+ # Resend verification
206
+ link = f"{HF_URL}?verify={token}"
207
+
208
+ email_sent = send_email(
209
+ email,
210
+ "πŸ” Verify your subscription",
211
+ f"""
212
+ <h3>Email Verification</h3>
213
+ <p>Click below to activate:</p>
214
+ <a href="{link}">Verify</a>
215
+ """
216
+ )
217
+
218
+ cur.close()
219
+ conn.close()
220
+
221
+ if email_sent:
222
+ return "πŸ“© Verification email re-sent"
223
+ else:
224
+ return "⚠️ Failed to send verification email"
225
+
226
+ # ---- New user ----
227
+
228
+ token = uuid.uuid4().hex
229
+
230
+ cur.execute("""
231
+ INSERT INTO users (
232
+ leetcode_username,
233
+ email,
234
+ timezone,
235
+ email_verified,
236
+ verification_token,
237
+ unsubscribed
238
+ )
239
+ VALUES (%s, %s, %s, false, %s, false)
240
+ """, (username, email, timezone, token))
241
+
242
+ conn.commit()
243
+ cur.close()
244
+ conn.close()
245
+
246
+ link = f"{HF_URL}?verify={token}"
247
+
248
+ email_sent = send_email(
249
+ email,
250
+ "βœ… Verify your subscription",
251
+ f"""
252
+ <h3>Welcome!</h3>
253
+ <p>Confirm your email:</p>
254
+ <a href="{link}">Verify Email</a>
255
+ """
256
+ )
257
+
258
+ if email_sent:
259
+ return "πŸ“© Verification email sent"
260
+ else:
261
+ return "⚠️ Subscription failed - could not send email"
262
+
263
+
264
+ # ================= VERIFY =================
265
+
266
+ def verify_user(token):
267
+
268
+ conn = get_db()
269
+ cur = conn.cursor()
270
+
271
+ cur.execute("""
272
+ UPDATE users
273
+ SET email_verified = true
274
+ WHERE verification_token = %s
275
+ AND email_verified = false
276
+ """, (token,))
277
+
278
+ affected = cur.rowcount
279
+ conn.commit()
280
+ cur.close()
281
+ conn.close()
282
+
283
+ if affected == 0:
284
+ return "❌ Invalid or already used verification link"
285
+
286
+ return "βœ… Email verified successfully!"
287
+
288
+
289
+ # ================= UNSUBSCRIBE =================
290
+
291
+ def unsubscribe_user(token):
292
+
293
+ conn = get_db()
294
+ cur = conn.cursor()
295
+
296
+ cur.execute("""
297
+ UPDATE users
298
+ SET unsubscribed = true
299
+ WHERE verification_token = %s
300
+ """, (token,))
301
+
302
+ affected = cur.rowcount
303
+
304
+ conn.commit()
305
+ cur.close()
306
+ conn.close()
307
+
308
+ if affected == 0:
309
+ return "❌ Invalid unsubscribe link"
310
+
311
+ return "βœ… You are unsubscribed"
312
+
313
+
314
+ # ================= SCHEDULER =================
315
+
316
+ def run_scheduler(secret):
317
+
318
+ if secret != CRON_SECRET:
319
+ return "❌ Unauthorized"
320
+
321
+ conn = get_db()
322
+ cur = conn.cursor()
323
+
324
+ cur.execute("""
325
+ SELECT id, leetcode_username, email, timezone,
326
+ last_sent_date, last_sent_slot,
327
+ verification_token
328
+ FROM users
329
+ WHERE email_verified = true
330
+ AND unsubscribed = false
331
+ """)
332
+
333
+ users = cur.fetchall()
334
+
335
+ title, slug = get_daily_problem()
336
+
337
+ now_utc = datetime.now(pytz.utc)
338
+
339
+ for uid, username, email, tz, last_date, last_slot, token in users:
340
+
341
+ local = now_utc.astimezone(pytz.timezone(tz))
342
+ hour = local.hour
343
+
344
+ if hour == 9:
345
+ slot = "morning"
346
+ subject = "πŸ“˜ Today's LeetCode"
347
+ body = f"<b>{title}</b>"
348
+
349
+ elif hour == 15:
350
+ slot = "afternoon"
351
+ subject = "⏰ Reminder"
352
+ body = f"You haven't solved <b>{title}</b> yet."
353
+
354
+ elif hour == 20:
355
+ slot = "night"
356
+ subject = "⚠️ Final Reminder"
357
+ body = f"Last chance to solve <b>{title}</b> today."
358
+
359
+ else:
360
+ continue
361
+
362
+ today = date.today()
363
+
364
+ # Idempotency
365
+ if last_date == today and last_slot == slot:
366
+ continue
367
+
368
+ if solved_today(username, slug):
369
+ continue
370
+
371
+ unsubscribe_link = f"{HF_URL}?unsubscribe={token}"
372
+
373
+ send_email(
374
+ email,
375
+ subject,
376
+ f"""
377
+ {body}
378
+ <br><br>
379
+ <a href="{unsubscribe_link}">Unsubscribe</a>
380
+ """
381
+ )
382
+
383
+ cur.execute("""
384
+ UPDATE users
385
+ SET last_sent_date=%s,
386
+ last_sent_slot=%s
387
+ WHERE id=%s
388
+ """, (today, slot, uid))
389
+
390
+ conn.commit()
391
+ cur.close()
392
+ conn.close()
393
+
394
+ return "βœ… Scheduler completed"
395
+
396
+
397
+ # ================= HANDLE URL PARAMS =================
398
+
399
+ def handle_url_params(request: gr.Request):
400
+ """Handle verify and unsubscribe links"""
401
+ try:
402
+ # Get query parameters from request
403
+ query_string = request.query_params
404
+
405
+ if "verify" in query_string:
406
+ token = query_string["verify"]
407
+ return verify_user(token)
408
+ elif "unsubscribe" in query_string:
409
+ token = query_string["unsubscribe"]
410
+ return unsubscribe_user(token)
411
+ else:
412
+ return ""
413
+ except Exception as e:
414
+ print(f"Error handling URL params: {e}")
415
+ return ""
416
+
417
+
418
+ # ================= UI =================
419
+
420
+ with gr.Blocks(title="LeetCode Daily Notifier") as app:
421
+
422
+ # Handle URL parameters at startup
423
+ url_message = gr.Markdown("")
424
+
425
+ gr.Markdown("## πŸ“¬ LeetCode Daily Email Notifier")
426
+
427
+ user = gr.Textbox(label="LeetCode Username")
428
+ mail = gr.Textbox(label="Email")
429
+ tz = gr.Dropdown(pytz.all_timezones, value="Asia/Kolkata", label="Timezone")
430
+
431
+ out = gr.Textbox(label="Status")
432
+
433
+ gr.Button("Subscribe").click(subscribe, [user, mail, tz], out)
434
+
435
+ gr.Markdown("### πŸ”’ Internal Scheduler")
436
+
437
+ secret = gr.Textbox(label="Cron Secret", type="password")
438
+
439
+ gr.Button("Run Scheduler").click(run_scheduler, secret, out)
440
+
441
+ # Handle URL parameters when app loads
442
+ app.load(handle_url_params, outputs=url_message)
443
+
444
+
445
+ if __name__ == "__main__":
446
+ app.launch(debug=True)