Pranesh64 commited on
Commit
d208f3c
Β·
verified Β·
1 Parent(s): a8a2838

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +793 -0
app.py ADDED
@@ -0,0 +1,793 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import json
4
+ import pickle
5
+ import base64
6
+ import threading
7
+ import time
8
+ import schedule
9
+ import psycopg2
10
+ import requests
11
+ import gradio as gr
12
+
13
+ from dotenv import load_dotenv
14
+ from email.mime.text import MIMEText
15
+ from datetime import datetime
16
+
17
+ from google_auth_oauthlib.flow import InstalledAppFlow
18
+ from google.auth.transport.requests import Request
19
+ from googleapiclient.discovery import build
20
+
21
+
22
+ # ================= ENV =================
23
+
24
+ load_dotenv()
25
+
26
+ DB_URL = os.getenv("DB_URL")
27
+ GMAIL_USER = os.getenv("GMAIL_USER")
28
+ HF_URL = os.getenv("HF_URL")
29
+ ADMIN_SECRET = os.getenv("ADMIN_SECRET")
30
+ XSRF_TOKEN = os.getenv("XSRF_TOKEN")
31
+ BIP_SESSION = os.getenv("BIP_SESSION")
32
+ BIP_API = "https://bip.bitsathy.ac.in/nova-api/student-activity-masters"
33
+
34
+ TOKEN_FILE = "token.pkl"
35
+
36
+ # COOKIE_FILE = "bip_cookies.json"
37
+ STATE_FILE = "state.json"
38
+ NEW_EVENTS_FILE = "new_events.json"
39
+ PAGE1_LOG_FILE = "page1_logs.json"
40
+
41
+ SCOPES = ["https://www.googleapis.com/auth/gmail.send"]
42
+
43
+ MAX_PAGES = 50
44
+ REQUEST_TIMEOUT = 20
45
+
46
+
47
+ # ================= DB =================
48
+
49
+ def get_db():
50
+ try:
51
+ return psycopg2.connect(DB_URL, sslmode="require")
52
+ except Exception as e:
53
+ print(f"❌ DB Connection error: {e}")
54
+ raise
55
+
56
+
57
+ # ================= COOKIES =================
58
+
59
+ def load_cookies():
60
+ """Load cookies from environment variables only"""
61
+ if not XSRF_TOKEN or not BIP_SESSION:
62
+ raise Exception("XSRF_TOKEN and BIP_SESSION must be set in environment variables")
63
+
64
+ return {
65
+ "XSRF-TOKEN": XSRF_TOKEN,
66
+ "bip_session": BIP_SESSION
67
+ }
68
+
69
+
70
+ # ================= GMAIL =================
71
+
72
+ def create_token():
73
+ if not os.path.exists("credentials.json"):
74
+ raise Exception("credentials.json missing")
75
+
76
+ flow = InstalledAppFlow.from_client_secrets_file(
77
+ "credentials.json",
78
+ SCOPES
79
+ )
80
+
81
+ creds = flow.run_local_server(port=0)
82
+
83
+ with open(TOKEN_FILE, "wb") as f:
84
+ pickle.dump(creds, f)
85
+
86
+ return creds
87
+
88
+
89
+ def get_gmail():
90
+ creds = None
91
+
92
+ if os.path.exists(TOKEN_FILE):
93
+ with open(TOKEN_FILE, "rb") as f:
94
+ creds = pickle.load(f)
95
+
96
+ if creds and creds.expired and creds.refresh_token:
97
+ creds.refresh(Request())
98
+
99
+ with open(TOKEN_FILE, "wb") as f:
100
+ pickle.dump(creds, f)
101
+
102
+ if not creds:
103
+ creds = create_token()
104
+
105
+ return build("gmail", "v1", credentials=creds)
106
+
107
+
108
+ def send_email(to, subject, html):
109
+ try:
110
+ service = get_gmail()
111
+
112
+ msg = MIMEText(html, "html")
113
+ msg["To"] = to
114
+ msg["From"] = GMAIL_USER
115
+ msg["Subject"] = subject
116
+
117
+ raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
118
+
119
+ service.users().messages().send(
120
+ userId="me",
121
+ body={"raw": raw}
122
+ ).execute()
123
+
124
+ print("βœ… Sent:", to)
125
+ return True
126
+
127
+ except Exception as e:
128
+ print("❌ Mail error:", e)
129
+ return False
130
+
131
+
132
+ # ================= EMAIL =================
133
+
134
+ def create_event_email(event, unsub):
135
+ return f"""
136
+ <h2>πŸ“’ New BIP Event</h2>
137
+ <b>{event['event_name']}</b>
138
+ <ul>
139
+ <li>Code: {event['event_code']}</li>
140
+ <li>Organizer: {event['organizer']}</li>
141
+ <li>Date: {event['start_date']}</li>
142
+ <li>Location: {event['location']}</li>
143
+ </ul>
144
+ <a href="{event['web_url']}">View</a>
145
+ <hr>
146
+ <a href="{unsub}">Unsubscribe</a>
147
+ """
148
+
149
+
150
+ # ================= BIP API =================
151
+
152
+ HEADERS = {
153
+ "accept": "application/json",
154
+ "x-requested-with": "XMLHttpRequest",
155
+ }
156
+
157
+ BASE_PARAMS = {
158
+ "perPage": 10
159
+ }
160
+
161
+
162
+ def fetch_page(page):
163
+ """Fetch a specific page from BIP API"""
164
+ cookies = load_cookies()
165
+
166
+ params = BASE_PARAMS.copy()
167
+ params["page"] = page
168
+
169
+ r = requests.get(
170
+ BIP_API,
171
+ params=params,
172
+ headers=HEADERS,
173
+ cookies=cookies,
174
+ timeout=REQUEST_TIMEOUT
175
+ )
176
+
177
+ # Expired session
178
+ if "text/html" in r.headers.get("Content-Type", ""):
179
+ raise Exception("Session expired. Login again.")
180
+
181
+ r.raise_for_status()
182
+
183
+ return r.json()
184
+
185
+
186
+ def fetch_latest():
187
+ """Legacy function - now uses fetch_page"""
188
+ return fetch_page(1)["resources"]
189
+
190
+
191
+ def parse_event(resource):
192
+ """Parse event resource into clean data structure"""
193
+ data = {}
194
+
195
+ for f in resource.get("fields", []):
196
+ key = f.get("attribute")
197
+ val = f.get("value")
198
+
199
+ if key:
200
+ data[key] = val
201
+
202
+ data["id"] = resource["id"]["value"]
203
+ data["title"] = resource.get("title")
204
+
205
+ return data
206
+
207
+
208
+ def fetch_new_events(old_id):
209
+ """Fetch all new events since the last known ID"""
210
+ page = 1
211
+ new_events = []
212
+
213
+ while page <= MAX_PAGES:
214
+ print(f"πŸ“„ Fetching page {page}...")
215
+
216
+ try:
217
+ data = fetch_page(page)
218
+ resources = data.get("resources", [])
219
+ except Exception as e:
220
+ print(f"❌ Error fetching page {page}: {e}")
221
+ break
222
+
223
+ if not resources:
224
+ break
225
+
226
+ for res in resources:
227
+ ev = parse_event(res)
228
+
229
+ if ev["id"] == old_id:
230
+ return new_events
231
+
232
+ new_events.append(ev)
233
+
234
+ page += 1
235
+
236
+ return new_events
237
+
238
+
239
+ # ================= STATE MANAGEMENT =================
240
+
241
+ def load_state():
242
+ """Load the last processed event state"""
243
+ try:
244
+ with open(STATE_FILE, "r") as f:
245
+ return json.load(f)
246
+ except:
247
+ return None
248
+
249
+
250
+ def save_state(latest_id):
251
+ """Save the latest processed event ID"""
252
+ state = {
253
+ "latest_id": latest_id,
254
+ "last_updated": datetime.now().isoformat()
255
+ }
256
+
257
+ with open(STATE_FILE, "w") as f:
258
+ json.dump(state, f, indent=2)
259
+
260
+
261
+ def save_new_events(events):
262
+ """Save new events to file"""
263
+ data = {
264
+ "timestamp": datetime.now().isoformat(),
265
+ "count": len(events),
266
+ "events": events
267
+ }
268
+
269
+ with open(NEW_EVENTS_FILE, "w") as f:
270
+ json.dump(data, f, indent=2, ensure_ascii=False)
271
+
272
+
273
+ # ================= PAGE 1 LOGGER =================
274
+
275
+ def load_page1_logs():
276
+ """Load page 1 historical logs"""
277
+ try:
278
+ with open(PAGE1_LOG_FILE, "r") as f:
279
+ return json.load(f)
280
+ except:
281
+ return []
282
+
283
+
284
+ def save_page1_logs(logs):
285
+ """Save page 1 logs to file"""
286
+ with open(PAGE1_LOG_FILE, "w") as f:
287
+ json.dump(logs, f, indent=2, ensure_ascii=False)
288
+
289
+
290
+ def log_page1_to_file():
291
+ """Save page 1 snapshot to file"""
292
+ try:
293
+ data = fetch_page(1)
294
+ resources = data.get("resources", [])
295
+
296
+ events = []
297
+ for res in resources:
298
+ events.append(parse_event(res))
299
+
300
+ logs = load_page1_logs()
301
+
302
+ entry = {
303
+ "timestamp": datetime.now().isoformat(),
304
+ "count": len(events),
305
+ "events": events
306
+ }
307
+
308
+ logs.append(entry)
309
+
310
+ # Keep only last 100 entries to prevent file from growing too large
311
+ if len(logs) > 100:
312
+ logs = logs[-100:]
313
+
314
+ save_page1_logs(logs)
315
+
316
+ print(f"πŸ“ Page-1 snapshot saved ({len(events)} events)")
317
+ return True
318
+
319
+ except Exception as e:
320
+ print(f"❌ Failed to log page 1: {e}")
321
+ return False
322
+
323
+
324
+ # ================= SUBSCRIBE =================
325
+
326
+ def subscribe(email):
327
+ if not email or not email.endswith("@bitsathy.ac.in"):
328
+ return "❌ Use college email only"
329
+
330
+ try:
331
+ db = get_db()
332
+ cur = db.cursor()
333
+
334
+ # Check if user already exists and is verified
335
+ cur.execute("""
336
+ SELECT email_verified, unsubscribed
337
+ FROM users
338
+ WHERE email=%s
339
+ """, (email,))
340
+
341
+ existing_user = cur.fetchone()
342
+
343
+ if existing_user:
344
+ verified, unsubscribed = existing_user
345
+
346
+ if verified and not unsubscribed:
347
+ cur.close()
348
+ db.close()
349
+ return "βœ… You're already subscribed to BIP alerts"
350
+
351
+ elif verified and unsubscribed:
352
+ # Re-subscribe previously unsubscribed user
353
+ cur.execute("""
354
+ UPDATE users
355
+ SET unsubscribed=false
356
+ WHERE email=%s
357
+ """, (email,))
358
+ db.commit()
359
+ cur.close()
360
+ db.close()
361
+ return "βœ… Successfully re-subscribed to BIP alerts"
362
+
363
+ elif not verified:
364
+ # User exists but not verified - generate new token and resend
365
+ token = uuid.uuid4().hex
366
+ cur.execute("""
367
+ UPDATE users
368
+ SET verification_token=%s
369
+ WHERE email=%s
370
+ """, (token, email))
371
+ db.commit()
372
+
373
+ link = f"{HF_URL}?verify={token}"
374
+
375
+ if send_email(
376
+ email,
377
+ "Verify BIP Alerts",
378
+ f"Click <a href='{link}'>here</a> to verify"
379
+ ):
380
+ result = "πŸ“© New verification email sent (previous one expired)"
381
+ else:
382
+ result = "❌ Failed to send verification email"
383
+
384
+ cur.close()
385
+ db.close()
386
+ return result
387
+
388
+ # New user - create account and send verification
389
+ token = uuid.uuid4().hex
390
+
391
+ cur.execute("""
392
+ INSERT INTO users(email, verification_token)
393
+ VALUES(%s, %s)
394
+ """, (email, token))
395
+
396
+ db.commit()
397
+
398
+ link = f"{HF_URL}?verify={token}"
399
+
400
+ if send_email(
401
+ email,
402
+ "Verify BIP Alerts",
403
+ f"Click <a href='{link}'>here</a> to verify"
404
+ ):
405
+ result = "πŸ“© Verification sent to your email"
406
+ else:
407
+ result = "❌ Failed to send verification email"
408
+
409
+ cur.close()
410
+ db.close()
411
+
412
+ return result
413
+
414
+ except Exception as e:
415
+ print(f"❌ Subscribe error: {e}")
416
+ return f"❌ Error: {str(e)}"
417
+
418
+ # ================= VERIFY =================
419
+
420
+ def verify_user(token):
421
+ if not token:
422
+ return ""
423
+
424
+ try:
425
+ db = get_db()
426
+ cur = db.cursor()
427
+
428
+ cur.execute("""
429
+ UPDATE users
430
+ SET email_verified=true,
431
+ verification_token=NULL,
432
+ unsubscribed=false
433
+ WHERE verification_token=%s
434
+ """, (token,))
435
+
436
+ if cur.rowcount > 0:
437
+ result = "βœ… Email verified successfully!"
438
+ else:
439
+ result = "❌ Invalid verification token"
440
+
441
+ db.commit()
442
+ cur.close()
443
+ db.close()
444
+
445
+ return result
446
+
447
+ except Exception as e:
448
+ print(f"❌ Verify error: {e}")
449
+ return f"❌ Verification failed: {str(e)}"
450
+
451
+
452
+ # ================= UNSUBSCRIBE =================
453
+
454
+ def unsubscribe_user(token):
455
+ if not token:
456
+ return ""
457
+
458
+ try:
459
+ db = get_db()
460
+ cur = db.cursor()
461
+
462
+ cur.execute("""
463
+ UPDATE users
464
+ SET unsubscribed=true
465
+ WHERE verification_token=%s OR email IN (
466
+ SELECT email FROM users WHERE verification_token=%s
467
+ )
468
+ """, (token, token))
469
+
470
+ if cur.rowcount > 0:
471
+ result = "βœ… Successfully unsubscribed from BIP alerts"
472
+ else:
473
+ result = "❌ Invalid unsubscribe link"
474
+
475
+ db.commit()
476
+ cur.close()
477
+ db.close()
478
+
479
+ return result
480
+
481
+ except Exception as e:
482
+ print(f"❌ Unsubscribe error: {e}")
483
+ return f"❌ Unsubscribe failed: {str(e)}"
484
+
485
+
486
+ # ================= EMAIL STATUS =================
487
+
488
+ def check_email_status(email):
489
+ if not email or not email.endswith("@bitsathy.ac.in"):
490
+ return "❌ Please enter a valid college email"
491
+
492
+ try:
493
+ db = get_db()
494
+ cur = db.cursor()
495
+
496
+ cur.execute("""
497
+ SELECT email_verified, unsubscribed, verification_token
498
+ FROM users
499
+ WHERE email=%s
500
+ """, (email,))
501
+
502
+ result = cur.fetchone()
503
+ cur.close()
504
+ db.close()
505
+
506
+ if not result:
507
+ return "πŸ“§ Email not found. Click Subscribe to register."
508
+
509
+ verified, unsubscribed, token = result
510
+
511
+ if unsubscribed:
512
+ return "🚫 Email is unsubscribed from alerts"
513
+ elif verified:
514
+ return "βœ… Email is verified and subscribed to alerts"
515
+ else:
516
+ return "⏳ Email registered but not verified. Check your inbox."
517
+
518
+ except Exception as e:
519
+ print(f"❌ Status check error: {e}")
520
+ return f"❌ Database error: {str(e)}"
521
+
522
+
523
+ # ================= ENHANCED NOTIFIER =================
524
+
525
+ def check_events():
526
+ """Enhanced event checker with state tracking"""
527
+ print("\nπŸ” Checking for new events...")
528
+
529
+ try:
530
+ # Save page 1 snapshot
531
+ log_page1_to_file()
532
+
533
+ state = load_state()
534
+
535
+ # First run
536
+ if not state:
537
+ print("πŸ†• First run detected")
538
+
539
+ data = fetch_page(1)
540
+ resources = data.get("resources", [])
541
+
542
+ if not resources:
543
+ print("⚠️ No events found")
544
+ return "⚠️ No events found on first run"
545
+
546
+ first_event = parse_event(resources[0])
547
+ save_state(first_event["id"])
548
+
549
+ print("πŸ“Œ Saved initial ID:", first_event["id"])
550
+ return f"πŸ“Œ Initialized with event ID: {first_event['id']}"
551
+
552
+ old_id = state["latest_id"]
553
+ print("πŸ“ Last known ID:", old_id)
554
+
555
+ new_events = fetch_new_events(old_id)
556
+
557
+ if not new_events:
558
+ print("⏳ No new events added")
559
+ return "⏳ No new events found"
560
+
561
+ latest = new_events[0]
562
+ print(f"\nπŸ”₯ {len(new_events)} NEW EVENT(S) ADDED!")
563
+ print("πŸ“’ Latest Event:", latest.get("event_name", "Unknown"))
564
+
565
+ # Save new events to file
566
+ save_new_events(new_events)
567
+
568
+ # Update state
569
+ save_state(latest["id"])
570
+
571
+ # Send notifications to users
572
+ return send_notifications_for_events(new_events)
573
+
574
+ except Exception as e:
575
+ error_msg = f"❌ Event check error: {e}"
576
+ print(error_msg)
577
+ return error_msg
578
+
579
+ def send_notifications_for_events(new_events):
580
+ """Send one combined email for all new events"""
581
+
582
+ if not new_events:
583
+ return "πŸ“­ No events to notify"
584
+
585
+ try:
586
+ db = get_db()
587
+ cur = db.cursor()
588
+
589
+ cur.execute("""
590
+ SELECT id,email,verification_token
591
+ FROM users
592
+ WHERE email_verified=true
593
+ AND unsubscribed=false
594
+ """)
595
+
596
+ users = cur.fetchall()
597
+
598
+ if not users:
599
+ cur.close()
600
+ db.close()
601
+ return "πŸ“­ No verified users"
602
+
603
+ # Build combined HTML
604
+ events_html = ""
605
+
606
+ for i, event in enumerate(new_events, 1):
607
+ events_html += f"""
608
+ <hr>
609
+ <h3>{i}. {event['event_name']}</h3>
610
+ <ul>
611
+ <li><b>Code:</b> {event['event_code']}</li>
612
+ <li><b>Event Name:</b> {event['event_name']}</li>
613
+ <li><b>Organizer:</b> {event['organizer']}</li>
614
+ <li><b>Date:</b> {event['start_date']}</li>
615
+ <li><b>Category:</b> {event['event_category']}</li>
616
+ <li><b>Location:</b> {event['location']}</li>
617
+ <li><b>View:</b> <a href="{event['web_url']}">Link</a></li>
618
+ </ul>
619
+ """
620
+
621
+ subject = f"πŸ“’ {len(new_events)} New BIP Events Added"
622
+
623
+ total_sent = 0
624
+
625
+ for uid, mail, token in users:
626
+
627
+ html = f"""
628
+ <h2>πŸ“’ New BIP Events Alert</h2>
629
+ <p>{len(new_events)} new events have been added:</p>
630
+ {events_html}
631
+ <hr>
632
+ <a href="{HF_URL}?unsubscribe={token}">Unsubscribe</a>
633
+ """
634
+
635
+ if send_email(mail, subject, html):
636
+ total_sent += 1
637
+
638
+ cur.close()
639
+ db.close()
640
+
641
+ return f"βœ… Sent {total_sent} combined notifications"
642
+
643
+ except Exception as e:
644
+ print("❌ Notification error:", e)
645
+ return f"❌ Error: {e}"
646
+
647
+ def run_notifier():
648
+ """Legacy function - now uses enhanced check_events"""
649
+ return check_events()
650
+
651
+
652
+
653
+ # ================= SCHEDULER =================
654
+
655
+ def scheduler_worker():
656
+ print("⏰ Scheduler started")
657
+
658
+ # Initial run
659
+ try:
660
+ result = check_events()
661
+ print(f"Initial check: {result}")
662
+ except Exception as e:
663
+ print(f"❌ Initial check failed: {e}")
664
+
665
+ # Schedule every 3 minutes
666
+ schedule.every(3).minutes.do(check_events)
667
+
668
+ while True:
669
+ try:
670
+ schedule.run_pending()
671
+ except Exception as e:
672
+ print(f"❌ Scheduler error: {e}")
673
+ time.sleep(30)
674
+
675
+
676
+ # ================= ROUTER =================
677
+
678
+ def route_handler(req: gr.Request):
679
+ try:
680
+ params = req.query_params
681
+
682
+ if "verify" in params:
683
+ return verify_user(params["verify"])
684
+
685
+ if "unsubscribe" in params:
686
+ return unsubscribe_user(params["unsubscribe"])
687
+
688
+ return ""
689
+
690
+ except Exception as e:
691
+ print(f"❌ Router error: {e}")
692
+ return f"❌ Error: {str(e)}"
693
+
694
+
695
+ # ================= UI =================
696
+
697
+ with gr.Blocks(title="BIP Notifier", theme=gr.themes.Soft()) as app:
698
+
699
+ gr.Markdown("# πŸ“’ BIP Event Email Alerts")
700
+ gr.Markdown("*Auto-checking every 3 minutes for new events*")
701
+
702
+ # ---------- Main Section ----------
703
+ with gr.Row():
704
+ with gr.Column():
705
+ email = gr.Textbox(
706
+ label="College Email (@bitsathy.ac.in)",
707
+ placeholder="your.name@bitsathy.ac.in"
708
+ )
709
+
710
+ with gr.Row():
711
+ sub_btn = gr.Button("Subscribe", variant="primary")
712
+ status_btn = gr.Button("Check Status", variant="secondary")
713
+
714
+ out = gr.Textbox(label="Status", interactive=False, lines=3)
715
+
716
+ sub_btn.click(subscribe, email, out)
717
+ status_btn.click(check_email_status, email, out)
718
+
719
+ # ---------- System Status ----------
720
+ with gr.Accordion("πŸ“Š System Status", open=False):
721
+ def get_system_status():
722
+ try:
723
+ state = load_state()
724
+ page1_logs = load_page1_logs()
725
+
726
+ status = "### Current State:\n"
727
+
728
+ if state:
729
+ status += f"- **Last Event ID**: {state.get('latest_id', 'N/A')}\n"
730
+ status += f"- **Last Updated**: {state.get('last_updated', 'N/A')}\n"
731
+ else:
732
+ status += "- **Status**: Not initialized\n"
733
+
734
+ status += f"- **Page 1 Logs**: {len(page1_logs)} entries\n"
735
+
736
+ if os.path.exists(NEW_EVENTS_FILE):
737
+ with open(NEW_EVENTS_FILE, 'r') as f:
738
+ new_events = json.load(f)
739
+ status += f"- **Last New Events**: {new_events.get('count', 0)} events\n"
740
+ else:
741
+ status += "- **Last New Events**: None\n"
742
+
743
+ return status
744
+
745
+ except Exception as e:
746
+ return f"❌ Error loading status: {e}"
747
+
748
+ status_btn = gr.Button("Get System Status")
749
+ status_out = gr.Textbox(label="System Status", lines=5, interactive=False)
750
+ status_btn.click(get_system_status, None, status_out)
751
+
752
+ # ---------- Instructions ----------
753
+ with gr.Accordion("πŸ“ Instructions", open=False):
754
+ gr.Markdown("""
755
+ ### How to use:
756
+ 1. Enter your college email (@bitsathy.ac.in)
757
+ 2. Click **Subscribe**
758
+ 3. Check your email and click the verification link
759
+ 4. You'll receive alerts for new BIP events automatically
760
+
761
+ ### Admin Setup:
762
+ 1. Login to BIP portal in browser
763
+ 2. Open Developer Tools (F12) β†’ Network tab
764
+ 3. Make any request and copy XSRF-TOKEN and bip_session cookies
765
+ 4. Paste them in Admin Controls above
766
+
767
+ ### Files Created:
768
+ - `state.json`: Tracks last processed event
769
+ - `page1_logs.json`: Historical snapshots of page 1
770
+ - `new_events.json`: Latest batch of new events found
771
+ """)
772
+
773
+ # ---------- URL Handler ----------
774
+ app.load(route_handler, None, out)
775
+
776
+
777
+ # ================= MAIN =================
778
+
779
+ if __name__ == "__main__":
780
+ print("πŸš€ BIP Event Notifier (Enhanced with File Tracking)")
781
+ print("=" * 50)
782
+
783
+ # Start scheduler in background
784
+ scheduler_thread = threading.Thread(
785
+ target=scheduler_worker,
786
+ daemon=True
787
+ )
788
+ scheduler_thread.start()
789
+
790
+ # Start Gradio app
791
+ app.launch(
792
+ share=False # Set to True for public sharing
793
+ )