Pranesh64 commited on
Commit
9222a6a
Β·
verified Β·
1 Parent(s): 9872ff1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -178
app.py CHANGED
@@ -2,22 +2,20 @@ 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
  import json
10
  import pickle
 
 
11
  from google.auth.transport.requests import Request
12
  from google.oauth2.credentials import Credentials
13
- from google_auth_oauthlib.flow import InstalledAppFlow
14
  from googleapiclient.discovery import build
15
- import base64
16
  from email.mime.text import MIMEText
17
- from email.mime.multipart import MIMEMultipart
18
  from datetime import datetime, date
19
  from dotenv import load_dotenv
20
- from urllib.parse import parse_qs
21
 
22
 
23
  # ================= LOAD ENV =================
@@ -26,12 +24,14 @@ load_dotenv()
26
 
27
  DB_URL = os.getenv("DB_URL")
28
  GMAIL_USER = os.getenv("GMAIL_USER")
29
- GMAIL_APP_PASSWORD = os.getenv("GMAIL_APP_PASSWORD")
30
  CRON_SECRET = os.getenv("CRON")
31
  HF_URL = os.getenv("HF_URL")
32
 
33
  LEETCODE_API = "https://leetcode-api-vercel.vercel.app"
34
 
 
 
 
35
 
36
  # ================= DB =================
37
 
@@ -39,61 +39,40 @@ def get_db():
39
  return psycopg2.connect(DB_URL, sslmode="require")
40
 
41
 
42
- SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
43
- TOKEN_FILE = "token.pkl"
44
-
45
 
46
  def get_gmail_service():
47
- """
48
- Get authenticated Gmail API service.
49
- Uses token.pkl if available.
50
- Recreates if expired using credentials from ENV.
51
- """
52
 
53
  creds = None
54
 
55
- # ---- Load cached token ----
56
  if os.path.exists(TOKEN_FILE):
57
  try:
58
  with open(TOKEN_FILE, "rb") as f:
59
  creds = pickle.load(f)
60
- except Exception:
61
  creds = None
62
 
63
- # ---- If token invalid ----
64
- if not creds or not creds.valid:
65
-
66
- if creds and creds.expired and creds.refresh_token:
67
- try:
68
- creds.refresh(Request())
69
- except Exception:
70
- creds = None
71
-
72
- # ---- Recreate using credentials ----
73
- if not creds:
74
-
75
- creds_json = os.getenv("GMAIL_CREDENTIALS")
76
-
77
- if not creds_json:
78
- raise Exception("Missing GMAIL_CREDENTIALS in secrets")
79
-
80
- creds_dict = json.loads(creds_json)
81
 
82
- flow = InstalledAppFlow.from_client_config(
83
- creds_dict,
84
- SCOPES,
85
- )
86
 
87
- creds = flow.run_local_server(port=0)
 
 
88
 
89
- # ---- Save token ----
90
- with open(TOKEN_FILE, "wb") as f:
91
- pickle.dump(creds, f)
 
92
 
93
  return build("gmail", "v1", credentials=creds)
94
 
95
 
96
-
97
  # ================= EMAIL =================
98
 
99
  def send_email(to, subject, html):
@@ -112,22 +91,17 @@ def send_email(to, subject, html):
112
 
113
  body = {"raw": raw}
114
 
115
- service.users().messages().send(
116
  userId="me",
117
  body=body
118
  ).execute()
119
 
120
- print("βœ… Gmail API sent:", to)
 
121
  return True
122
 
123
  except Exception as e:
124
- print("❌ Gmail API error:", e)
125
-
126
- # If token is corrupted β†’ delete and retry next time
127
- if os.path.exists(TOKEN_FILE):
128
- os.remove(TOKEN_FILE)
129
- print("♻️ token.pkl deleted, will regenerate")
130
-
131
  return False
132
 
133
 
@@ -153,14 +127,9 @@ def valid_leetcode(username):
153
  timeout=8
154
  )
155
 
156
- if r.status_code != 200:
157
- return False
158
-
159
- data = r.json()
160
-
161
- return "username" in data or "profile" in data
162
 
163
- except Exception:
164
  return False
165
 
166
 
@@ -195,24 +164,18 @@ def solved_today(username, slug):
195
 
196
  data = r.json()
197
 
198
- # βœ… New API format (your case)
199
  if isinstance(data, dict) and "submission" in data:
200
  submissions = data["submission"]
201
 
202
- # βœ… Old API format
203
  elif isinstance(data, dict) and "data" in data:
204
  submissions = data["data"]
205
 
206
- # βœ… Direct list format
207
  elif isinstance(data, list):
208
  submissions = data
209
 
210
- # ❌ Error / invalid format
211
  else:
212
- print("⚠️ Unknown submission format:", data)
213
  return False
214
 
215
- # Get today's timestamp (Unix day start)
216
  today = date.today()
217
 
218
  for s in submissions:
@@ -228,7 +191,6 @@ def solved_today(username, slug):
228
  if not ts:
229
  continue
230
 
231
- # Convert Unix timestamp οΏ½οΏ½οΏ½ date
232
  solved_date = datetime.fromtimestamp(
233
  int(ts),
234
  tz=pytz.utc
@@ -257,7 +219,6 @@ def subscribe(username, email, timezone):
257
  conn = get_db()
258
  cur = conn.cursor()
259
 
260
- # ---- Duplicate / Rate limit ----
261
  cur.execute("""
262
  SELECT email_verified, verification_token, unsubscribed
263
  FROM users
@@ -266,59 +227,50 @@ def subscribe(username, email, timezone):
266
 
267
  row = cur.fetchone()
268
 
269
- # ---- Existing user ----
270
  if row:
271
 
272
- verified, token, unsubscribed = row
273
 
274
- # Already active
275
- if verified and not unsubscribed:
276
  cur.close()
277
  conn.close()
278
  return "⚠️ Already subscribed"
279
 
280
- # Was unsubscribed β†’ resubscribe
281
- if verified and unsubscribed:
282
-
283
  cur.execute("""
284
  UPDATE users
285
- SET unsubscribed = false,
286
- leetcode_username = %s,
287
- timezone = %s
288
- WHERE email = %s
 
 
289
  """, (username, timezone, email))
290
 
291
-
292
  conn.commit()
293
  cur.close()
294
  conn.close()
295
-
296
- return "βœ… You have been re-subscribed!"
297
 
 
298
 
299
- # Resend verification
300
  link = f"{HF_URL}?verify={token}"
301
 
302
- email_sent = send_email(
303
  email,
304
- "πŸ” Verify your subscription",
305
- f"""
306
- <h3>Email Verification</h3>
307
- <p>Click below to activate:</p>
308
- <a href="{link}">Verify</a>
309
- """
310
  )
311
 
312
  cur.close()
313
  conn.close()
314
-
315
- if email_sent:
316
- return "πŸ“© Verification email re-sent"
317
- else:
318
- return "⚠️ Failed to send verification email"
319
 
320
- # ---- New user ----
321
 
 
322
  token = uuid.uuid4().hex
323
 
324
  cur.execute("""
@@ -330,7 +282,7 @@ def subscribe(username, email, timezone):
330
  verification_token,
331
  unsubscribed
332
  )
333
- VALUES (%s, %s, %s, false, %s, false)
334
  """, (username, email, timezone, token))
335
 
336
  conn.commit()
@@ -339,20 +291,13 @@ def subscribe(username, email, timezone):
339
 
340
  link = f"{HF_URL}?verify={token}"
341
 
342
- email_sent = send_email(
343
  email,
344
- "βœ… Verify your subscription",
345
- f"""
346
- <h3>Welcome!</h3>
347
- <p>Confirm your email:</p>
348
- <a href="{link}">Verify Email</a>
349
- """
350
  )
351
 
352
- if email_sent:
353
- return "πŸ“© Verification email sent"
354
- else:
355
- return "⚠️ Subscription failed - could not send email"
356
 
357
 
358
  # ================= VERIFY =================
@@ -364,20 +309,21 @@ def verify_user(token):
364
 
365
  cur.execute("""
366
  UPDATE users
367
- SET email_verified = true
368
- WHERE verification_token = %s
369
- AND email_verified = false
370
  """, (token,))
371
 
372
- affected = cur.rowcount
 
373
  conn.commit()
374
  cur.close()
375
  conn.close()
376
 
377
- if affected == 0:
378
- return "❌ Invalid or already used verification link"
379
 
380
- return "βœ… Email verified successfully!"
381
 
382
 
383
  # ================= UNSUBSCRIBE =================
@@ -389,20 +335,20 @@ def unsubscribe_user(token):
389
 
390
  cur.execute("""
391
  UPDATE users
392
- SET unsubscribed = true
393
- WHERE verification_token = %s
394
  """, (token,))
395
 
396
- affected = cur.rowcount
397
 
398
  conn.commit()
399
  cur.close()
400
  conn.close()
401
 
402
- if affected == 0:
403
- return "❌ Invalid unsubscribe link"
404
 
405
- return "βœ… You are unsubscribed"
406
 
407
 
408
  # ================= SCHEDULER =================
@@ -420,60 +366,61 @@ def run_scheduler(secret):
420
  last_sent_date, last_sent_slot,
421
  verification_token
422
  FROM users
423
- WHERE email_verified = true
424
- AND unsubscribed = false
425
  """)
426
 
427
  users = cur.fetchall()
428
 
429
  title, slug = get_daily_problem()
430
 
431
- now_utc = datetime.now(pytz.utc)
432
 
433
- for uid, username, email, tz, last_date, last_slot, token in users:
434
 
435
- local = now_utc.astimezone(pytz.timezone(tz))
 
 
436
  hour = local.hour
437
 
438
  if hour == 9:
439
  slot = "morning"
440
- subject = "πŸ“˜ Today's LeetCode"
441
  body = f"<b>{title}</b>"
442
 
443
  elif hour == 15:
444
  slot = "afternoon"
445
- subject = "⏰ Reminder"
446
- body = f"You haven't solved <b>{title}</b> yet."
447
 
448
  elif hour == 20:
449
  slot = "night"
450
- subject = "⚠️ Final Reminder"
451
- body = f"Last chance to solve <b>{title}</b> today."
452
 
453
  else:
454
  continue
455
 
456
  today = date.today()
457
 
458
- # Idempotency
459
- if last_date == today and last_slot == slot:
460
  continue
461
 
462
- if solved_today(username, slug):
463
  continue
464
 
465
- unsubscribe_link = f"{HF_URL}?unsubscribe={token}"
466
 
467
- send_email(
468
- email,
469
- subject,
470
- f"""
471
- {body}
472
- <br><br>
473
- <a href="{unsubscribe_link}">Unsubscribe</a>
474
- """
475
  )
476
 
 
 
 
 
477
  cur.execute("""
478
  UPDATE users
479
  SET last_sent_date=%s,
@@ -481,60 +428,35 @@ def run_scheduler(secret):
481
  WHERE id=%s
482
  """, (today, slot, uid))
483
 
 
 
484
  conn.commit()
485
  cur.close()
486
  conn.close()
487
 
488
- return "βœ… Scheduler completed"
489
-
490
-
491
- # ================= HANDLE URL PARAMS =================
492
-
493
- def handle_url_params(request: gr.Request):
494
- """Handle verify and unsubscribe links"""
495
- try:
496
- # Get query parameters from request
497
- query_string = request.query_params
498
-
499
- if "verify" in query_string:
500
- token = query_string["verify"]
501
- return verify_user(token)
502
- elif "unsubscribe" in query_string:
503
- token = query_string["unsubscribe"]
504
- return unsubscribe_user(token)
505
- else:
506
- return ""
507
- except Exception as e:
508
- print(f"Error handling URL params: {e}")
509
- return ""
510
 
511
 
512
  # ================= UI =================
513
 
514
- with gr.Blocks(title="LeetCode Daily Notifier") as app:
515
 
516
- # Handle URL parameters at startup
517
- url_message = gr.Markdown("")
518
-
519
  gr.Markdown("## πŸ“¬ LeetCode Daily Email Notifier")
520
 
521
- user = gr.Textbox(label="LeetCode Username")
522
  mail = gr.Textbox(label="Email")
523
- tz = gr.Dropdown(pytz.all_timezones, value="Asia/Kolkata", label="Timezone")
524
 
525
- out = gr.Textbox(label="Status")
526
 
527
  gr.Button("Subscribe").click(subscribe, [user, mail, tz], out)
528
 
529
- gr.Markdown("### πŸ”’ Internal Scheduler")
530
-
531
- secret = gr.Textbox(label="Cron Secret", type="password")
532
 
533
- gr.Button("Run Scheduler").click(run_scheduler, secret, out)
534
 
535
- # Handle URL parameters when app loads
536
- app.load(handle_url_params, outputs=url_message)
537
 
538
 
539
  if __name__ == "__main__":
540
- app.launch(debug=True)
 
2
  import re
3
  import uuid
4
  import pytz
 
5
  import psycopg2
6
  import requests
7
  import gradio as gr
8
  import json
9
  import pickle
10
+ import base64
11
+
12
  from google.auth.transport.requests import Request
13
  from google.oauth2.credentials import Credentials
 
14
  from googleapiclient.discovery import build
15
+
16
  from email.mime.text import MIMEText
 
17
  from datetime import datetime, date
18
  from dotenv import load_dotenv
 
19
 
20
 
21
  # ================= LOAD ENV =================
 
24
 
25
  DB_URL = os.getenv("DB_URL")
26
  GMAIL_USER = os.getenv("GMAIL_USER")
 
27
  CRON_SECRET = os.getenv("CRON")
28
  HF_URL = os.getenv("HF_URL")
29
 
30
  LEETCODE_API = "https://leetcode-api-vercel.vercel.app"
31
 
32
+ SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
33
+ TOKEN_FILE = "token.pkl"
34
+
35
 
36
  # ================= DB =================
37
 
 
39
  return psycopg2.connect(DB_URL, sslmode="require")
40
 
41
 
42
+ # ================= GMAIL AUTH =================
 
 
43
 
44
  def get_gmail_service():
 
 
 
 
 
45
 
46
  creds = None
47
 
48
+ # Load token
49
  if os.path.exists(TOKEN_FILE):
50
  try:
51
  with open(TOKEN_FILE, "rb") as f:
52
  creds = pickle.load(f)
53
+ except:
54
  creds = None
55
 
56
+ # Refresh token
57
+ if creds and creds.expired and creds.refresh_token:
58
+ try:
59
+ creds.refresh(Request())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ with open(TOKEN_FILE, "wb") as f:
62
+ pickle.dump(creds, f)
 
 
63
 
64
+ except Exception as e:
65
+ print("❌ Token refresh failed:", e)
66
+ creds = None
67
 
68
+ if not creds:
69
+ raise Exception(
70
+ "❌ Missing token.pkl. Generate locally and upload to HF."
71
+ )
72
 
73
  return build("gmail", "v1", credentials=creds)
74
 
75
 
 
76
  # ================= EMAIL =================
77
 
78
  def send_email(to, subject, html):
 
91
 
92
  body = {"raw": raw}
93
 
94
+ result = service.users().messages().send(
95
  userId="me",
96
  body=body
97
  ).execute()
98
 
99
+ print("βœ… Gmail sent:", to, "| ID:", result.get("id"))
100
+
101
  return True
102
 
103
  except Exception as e:
104
+ print("❌ Gmail send failed:", e)
 
 
 
 
 
 
105
  return False
106
 
107
 
 
127
  timeout=8
128
  )
129
 
130
+ return r.status_code == 200
 
 
 
 
 
131
 
132
+ except:
133
  return False
134
 
135
 
 
164
 
165
  data = r.json()
166
 
 
167
  if isinstance(data, dict) and "submission" in data:
168
  submissions = data["submission"]
169
 
 
170
  elif isinstance(data, dict) and "data" in data:
171
  submissions = data["data"]
172
 
 
173
  elif isinstance(data, list):
174
  submissions = data
175
 
 
176
  else:
 
177
  return False
178
 
 
179
  today = date.today()
180
 
181
  for s in submissions:
 
191
  if not ts:
192
  continue
193
 
 
194
  solved_date = datetime.fromtimestamp(
195
  int(ts),
196
  tz=pytz.utc
 
219
  conn = get_db()
220
  cur = conn.cursor()
221
 
 
222
  cur.execute("""
223
  SELECT email_verified, verification_token, unsubscribed
224
  FROM users
 
227
 
228
  row = cur.fetchone()
229
 
 
230
  if row:
231
 
232
+ verified, token, unsub = row
233
 
234
+ # Active
235
+ if verified and not unsub:
236
  cur.close()
237
  conn.close()
238
  return "⚠️ Already subscribed"
239
 
240
+ # Resubscribe
241
+ if verified and unsub:
242
+
243
  cur.execute("""
244
  UPDATE users
245
+ SET unsubscribed=false,
246
+ leetcode_username=%s,
247
+ timezone=%s,
248
+ last_sent_date=NULL,
249
+ last_sent_slot=NULL
250
+ WHERE email=%s
251
  """, (username, timezone, email))
252
 
 
253
  conn.commit()
254
  cur.close()
255
  conn.close()
 
 
256
 
257
+ return "βœ… Re-subscribed successfully!"
258
 
259
+ # Resend verify
260
  link = f"{HF_URL}?verify={token}"
261
 
262
+ send_email(
263
  email,
264
+ "Verify your subscription",
265
+ f"<a href='{link}'>Verify</a>"
 
 
 
 
266
  )
267
 
268
  cur.close()
269
  conn.close()
 
 
 
 
 
270
 
271
+ return "πŸ“© Verification re-sent"
272
 
273
+ # New user
274
  token = uuid.uuid4().hex
275
 
276
  cur.execute("""
 
282
  verification_token,
283
  unsubscribed
284
  )
285
+ VALUES (%s,%s,%s,false,%s,false)
286
  """, (username, email, timezone, token))
287
 
288
  conn.commit()
 
291
 
292
  link = f"{HF_URL}?verify={token}"
293
 
294
+ send_email(
295
  email,
296
+ "Verify your subscription",
297
+ f"<a href='{link}'>Verify</a>"
 
 
 
 
298
  )
299
 
300
+ return "πŸ“© Verification sent"
 
 
 
301
 
302
 
303
  # ================= VERIFY =================
 
309
 
310
  cur.execute("""
311
  UPDATE users
312
+ SET email_verified=true
313
+ WHERE verification_token=%s
314
+ AND email_verified=false
315
  """, (token,))
316
 
317
+ updated = cur.rowcount
318
+
319
  conn.commit()
320
  cur.close()
321
  conn.close()
322
 
323
+ if updated == 0:
324
+ return "❌ Invalid link"
325
 
326
+ return "βœ… Email verified"
327
 
328
 
329
  # ================= UNSUBSCRIBE =================
 
335
 
336
  cur.execute("""
337
  UPDATE users
338
+ SET unsubscribed=true
339
+ WHERE verification_token=%s
340
  """, (token,))
341
 
342
+ updated = cur.rowcount
343
 
344
  conn.commit()
345
  cur.close()
346
  conn.close()
347
 
348
+ if updated == 0:
349
+ return "❌ Invalid link"
350
 
351
+ return "βœ… Unsubscribed"
352
 
353
 
354
  # ================= SCHEDULER =================
 
366
  last_sent_date, last_sent_slot,
367
  verification_token
368
  FROM users
369
+ WHERE email_verified=true
370
+ AND unsubscribed=false
371
  """)
372
 
373
  users = cur.fetchall()
374
 
375
  title, slug = get_daily_problem()
376
 
377
+ now = datetime.now(pytz.utc)
378
 
379
+ sent = 0
380
 
381
+ for uid, user, mail, tz, last_d, last_s, token in users:
382
+
383
+ local = now.astimezone(pytz.timezone(tz))
384
  hour = local.hour
385
 
386
  if hour == 9:
387
  slot = "morning"
388
+ sub = "Today's LeetCode"
389
  body = f"<b>{title}</b>"
390
 
391
  elif hour == 15:
392
  slot = "afternoon"
393
+ sub = "Reminder"
394
+ body = f"Solve <b>{title}</b>"
395
 
396
  elif hour == 20:
397
  slot = "night"
398
+ sub = "Final Reminder"
399
+ body = f"Last chance: <b>{title}</b>"
400
 
401
  else:
402
  continue
403
 
404
  today = date.today()
405
 
406
+ if last_d == today and last_s == slot:
 
407
  continue
408
 
409
+ if solved_today(user, slug):
410
  continue
411
 
412
+ unsub = f"{HF_URL}?unsubscribe={token}"
413
 
414
+ ok = send_email(
415
+ mail,
416
+ sub,
417
+ f"{body}<br><a href='{unsub}'>Unsubscribe</a>"
 
 
 
 
418
  )
419
 
420
+ if not ok:
421
+ print("❌ Failed:", mail)
422
+ continue
423
+
424
  cur.execute("""
425
  UPDATE users
426
  SET last_sent_date=%s,
 
428
  WHERE id=%s
429
  """, (today, slot, uid))
430
 
431
+ sent += 1
432
+
433
  conn.commit()
434
  cur.close()
435
  conn.close()
436
 
437
+ return f"βœ… Scheduler completed. Sent: {sent}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
 
439
 
440
  # ================= UI =================
441
 
442
+ with gr.Blocks(title="LeetCode Notifier") as app:
443
 
 
 
 
444
  gr.Markdown("## πŸ“¬ LeetCode Daily Email Notifier")
445
 
446
+ user = gr.Textbox(label="Username")
447
  mail = gr.Textbox(label="Email")
448
+ tz = gr.Dropdown(pytz.all_timezones, value="Asia/Kolkata")
449
 
450
+ out = gr.Textbox()
451
 
452
  gr.Button("Subscribe").click(subscribe, [user, mail, tz], out)
453
 
454
+ gr.Markdown("### πŸ”’ Scheduler")
 
 
455
 
456
+ sec = gr.Textbox(type="password")
457
 
458
+ gr.Button("Run").click(run_scheduler, sec, out)
 
459
 
460
 
461
  if __name__ == "__main__":
462
+ app.launch(debug=True)