DinoPLayZ commited on
Commit
cf3514c
·
verified ·
1 Parent(s): 42999db

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +432 -408
main.py CHANGED
@@ -1,409 +1,433 @@
1
- import os
2
- import time
3
- import argparse
4
- import requests
5
- from datetime import datetime
6
- from dotenv import load_dotenv
7
-
8
- # Load optional .env if present in same directory
9
- load_dotenv()
10
-
11
- # ==============================================================================
12
- # CONFIGURATION
13
- # ==============================================================================
14
- # Environment values (Make sure to populate your .env file)
15
- # We will reload these inside the loop so you can change them on the fly.
16
-
17
- # Telegram settings (Optional, loaded from .env or hardcoded here)
18
- TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
19
- TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
20
-
21
- # App check interval in seconds (default 60 secs = 1 min)
22
- CHECK_INTERVAL_SECONDS = 60
23
-
24
- BIP_API = "https://bip.bitsathy.ac.in/nova-api/student-activity-masters"
25
- HEADERS = {
26
- "accept": "application/json",
27
- "x-requested-with": "XMLHttpRequest",
28
- }
29
-
30
- # State tracking for the loop
31
- LAST_EVENT_ID = None
32
- LAST_EVENT_CODE = None
33
- SESSION_EXPIRED = False
34
- EXPIRED_XSRF = None
35
- EXPIRED_BIP = None
36
-
37
-
38
- # ==============================================================================
39
- # TELEGRAM NOTIFIER
40
- # ==============================================================================
41
- TELEGRAM_OFFSET = None
42
-
43
- def update_env_file(new_xsrf, new_bip):
44
- env_path = ".env"
45
- if not os.path.exists(env_path):
46
- with open(env_path, "w") as f:
47
- if new_xsrf: f.write(f'XSRF_TOKEN="{new_xsrf}"\n')
48
- if new_bip: f.write(f'BIP_SESSION="{new_bip}"\n')
49
- return
50
-
51
- with open(env_path, "r") as f:
52
- lines = f.readlines()
53
-
54
- with open(env_path, "w") as f:
55
- xsrf_updated = False
56
- bip_updated = False
57
- for line in lines:
58
- if line.startswith("XSRF_TOKEN=") and new_xsrf is not None:
59
- f.write(f'XSRF_TOKEN="{new_xsrf}"\n')
60
- xsrf_updated = True
61
- elif line.startswith("BIP_SESSION=") and new_bip is not None:
62
- f.write(f'BIP_SESSION="{new_bip}"\n')
63
- bip_updated = True
64
- else:
65
- f.write(line)
66
- if new_xsrf is not None and not xsrf_updated:
67
- f.write(f'XSRF_TOKEN="{new_xsrf}"\n')
68
- if new_bip is not None and not bip_updated:
69
- f.write(f'BIP_SESSION="{new_bip}"\n')
70
-
71
- def check_telegram_messages():
72
- global TELEGRAM_OFFSET, SESSION_EXPIRED, EXPIRED_XSRF, EXPIRED_BIP
73
- if not TELEGRAM_BOT_TOKEN: return False
74
-
75
- url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/getUpdates"
76
- params = {"timeout": 5}
77
- if TELEGRAM_OFFSET:
78
- params["offset"] = TELEGRAM_OFFSET
79
-
80
- try:
81
- r = requests.get(url, params=params, timeout=10)
82
- data = r.json()
83
- cookies_updated = False
84
- if data.get("ok"):
85
- for result in data.get("result", []):
86
- TELEGRAM_OFFSET = result["update_id"] + 1
87
- msg = result.get("message", {})
88
- text = msg.get("text", "").strip()
89
- chat_id = msg.get("chat", {}).get("id")
90
-
91
- # Only process messages from our authorized user
92
- if str(chat_id) == str(TELEGRAM_CHAT_ID):
93
- new_xsrf = None
94
- new_bip = None
95
- for line in text.replace("\r", "\n").split("\n"):
96
- line = line.strip()
97
- if line.startswith("XSRF_TOKEN=") or line.startswith("XSRF-TOKEN="):
98
- parts = line.split("=", 1)
99
- if len(parts) > 1:
100
- new_xsrf = parts[1].strip(' "\'')
101
- elif line.startswith("BIP_SESSION=") or line.startswith("bip_session="):
102
- parts = line.split("=", 1)
103
- if len(parts) > 1:
104
- new_bip = parts[1].strip(' "\'')
105
-
106
- if new_xsrf or new_bip:
107
- update_env_file(new_xsrf, new_bip)
108
- send_telegram_message("✅ <b>Cookies Received!</b>\n\nI have updated the <code>.env</code> file and will now resume checking the BIP portal.")
109
- SESSION_EXPIRED = False
110
- EXPIRED_XSRF = None
111
- EXPIRED_BIP = None
112
- cookies_updated = True
113
- print(f"[{datetime.now().strftime('%I:%M:%S %p')}] 🔄 Cookies received via Telegram! Resuming checks...")
114
- return cookies_updated
115
- except Exception as e:
116
- return False
117
-
118
- def send_telegram_message(text: str):
119
- if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
120
- print("\n[!] Telegram not configured. The following alert would have been sent:\n")
121
- print(text)
122
- print("-" * 50)
123
- return False
124
-
125
- url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
126
- payload = {
127
- "chat_id": TELEGRAM_CHAT_ID,
128
- "text": text,
129
- "parse_mode": "HTML",
130
- "disable_web_page_preview": True
131
- }
132
-
133
- try:
134
- r = requests.post(url, json=payload, timeout=10)
135
- r.raise_for_status()
136
- return True
137
- except Exception as e:
138
- print(f"[!] Failed to send telegram message: {e}")
139
- return False
140
-
141
- def send_event_alerts(events):
142
- if not events:
143
- return
144
-
145
- msg = f"📢 <b>{len(events)} New BIP Event Found!</b>\n\n"
146
-
147
- for i, ev in enumerate(events, 1):
148
- # Format start date
149
- start = ev.get('start_date', '')
150
- if start:
151
- start = datetime.strptime(start, "%Y-%m-%d").strftime("%d-%m-%Y")
152
-
153
- # Format end date
154
- end = ev.get('end_date', '')
155
- if end:
156
- end = datetime.strptime(end, "%Y-%m-%d").strftime("%d-%m-%Y")
157
-
158
- msg += (
159
- f"<b>{ev.get('event_code', '-')} - "
160
- f"{ev.get('event_name', 'Unknown')}</b>\n\n"
161
- f"{ev.get('event_category', '-')}\n\n"
162
- f"{start} to {end}\n\n"
163
- f"{ev.get('location', '-')}\n\n"
164
- f"{ev.get('status', '-')}\n\n"
165
- f"Seats: Max {ev.get('maximum_count', '-')} | "
166
- f"Applied {ev.get('applied_count', '-')}\n"
167
- f"<a href='{ev.get('web_url', '#')}'>View Event Here</a>\n"
168
- f"────────────────\n"
169
- )
170
- send_telegram_message(msg)
171
-
172
-
173
- # ==============================================================================
174
- # SCRAPER LOGIC
175
- # ==============================================================================
176
- def fetch_bip_events(xsrf_token, bip_session, page=1):
177
- cookies = {
178
- "XSRF-TOKEN": xsrf_token,
179
- "bip_session": bip_session
180
- }
181
- params = {"perPage": 10, "page": page}
182
- try:
183
- r = requests.get(BIP_API, params=params, headers=HEADERS, cookies=cookies, timeout=20)
184
- # Check if session expired based on HTML redirect instead of JSON
185
- if "text/html" in r.headers.get("Content-Type", "") or r.status_code == 401 or r.status_code == 403:
186
- return None, "Session expired or invalid cookies."
187
- r.raise_for_status()
188
- return r.json(), None
189
- except Exception as e:
190
- return None, str(e)
191
-
192
- def parse_event(resource):
193
- data = {}
194
- for f in resource.get("fields", []):
195
- key = f.get("attribute")
196
- val = f.get("value")
197
- if key:
198
- data[key] = val
199
- data["id"] = resource["id"]["value"]
200
- return data
201
-
202
- def check_new_events(last_id, xsrf_token, bip_session):
203
- """Fetches events and returns any newer than last_id. Automatically paginates if needed."""
204
- new_events = []
205
- page = 1
206
-
207
- while page <= 10: # Limit to 10 pages for safety
208
- data, err = fetch_bip_events(xsrf_token, bip_session, page)
209
- if err or not data:
210
- return None, err
211
-
212
- resources = data.get("resources", [])
213
- if not resources:
214
- break
215
-
216
- for res in resources:
217
- ev = parse_event(res)
218
- # Stop if we reach the last known new event
219
- if last_id and str(ev["id"]) == str(last_id):
220
- return new_events, None
221
-
222
- new_events.append(ev)
223
-
224
- # First-ever run scenario: just return the latest event to set the initial ID and avoid sending 100s of alerts
225
- if not last_id and new_events:
226
- return [new_events[0]], None
227
-
228
- page += 1
229
-
230
- return new_events, None
231
-
232
-
233
- # ==============================================================================
234
- # SCHEDULER ENGINE
235
- # ==============================================================================
236
- def process_tick():
237
- global LAST_EVENT_ID, LAST_EVENT_CODE, SESSION_EXPIRED, EXPIRED_XSRF, EXPIRED_BIP
238
- # Reload environment variables on every tick so user can update .env without restarting
239
- load_dotenv(override=True)
240
- xsrf = os.getenv("XSRF_TOKEN", "")
241
- bip = os.getenv("BIP_SESSION", "")
242
-
243
- now = datetime.now()
244
- time_str = now.strftime('%I:%M:%S %p')
245
-
246
- # Optional logic: only run between 8AM and 6PM
247
- if now.hour < 8 or now.hour >= 18:
248
- print(f"[{time_str}] 🌙 Out of hours (8 AM - 6 PM). Skipping check.")
249
- return
250
-
251
- if not xsrf or not bip:
252
- print(f"[{time_str}] ⏳ Skipping check: Please configure XSRF_TOKEN and BIP_SESSION in the .env file.")
253
- return
254
-
255
- # Unsilenced print block to report resuming checking!
256
- if SESSION_EXPIRED:
257
- if xsrf == EXPIRED_XSRF and bip == EXPIRED_BIP:
258
- print(f"{time_str.lower()} - ⏸️ Paused: Waiting for new cookies in .env to resume...")
259
- return
260
- else:
261
- print(f"{time_str.lower()} - 🔄 New cookies detected! Resuming checks...")
262
- SESSION_EXPIRED = False
263
- EXPIRED_XSRF = None
264
- EXPIRED_BIP = None
265
-
266
- new_events, err = check_new_events(LAST_EVENT_ID, xsrf, bip)
267
-
268
- if err:
269
- print(f"{time_str.lower()} - ❌ Error scraping events: {err}")
270
- send_telegram_message(
271
- "⚠️ <b>Scraper Error!</b>\n\n"
272
- f"The notifier encountered an error and has paused checking. Error:\n"
273
- f"<code>{err}</code>\n\n"
274
- "Please reply directly to this bot with your new cookies in this format to resume:\n\n"
275
- "<code>XSRF_TOKEN=your_new_token_here\nBIP_SESSION=your_new_session_here</code>"
276
- )
277
- SESSION_EXPIRED = True
278
- EXPIRED_XSRF = xsrf
279
- EXPIRED_BIP = bip
280
- return
281
-
282
- if new_events:
283
- # If LAST_EVENT_ID is None, it's the very first startup run. Set ID without alerting.
284
- if LAST_EVENT_ID is None:
285
- LAST_EVENT_ID = new_events[0]["id"]
286
- LAST_EVENT_CODE = new_events[0].get('event_code', LAST_EVENT_ID)
287
- print(f"{time_str.lower()} - EVENT ID : {LAST_EVENT_CODE} (Tracking started)")
288
- else:
289
- send_event_alerts(new_events)
290
- LAST_EVENT_ID = new_events[0]["id"]
291
- LAST_EVENT_CODE = new_events[0].get('event_code', LAST_EVENT_ID)
292
- for ev in new_events:
293
- code = ev.get('event_code', ev['id'])
294
- print(f"{time_str.lower()} - 🚨 NEW EVENT ID : {code} (Alert Sent!)")
295
- else:
296
- # Just print the tracking status format exactly as requested on every 1-minute tick
297
- print(f"{time_str.lower()} - EVENT ID : {LAST_EVENT_CODE}")
298
-
299
- def list_all_events():
300
- """Fetches the first page of events from BIP and prints them."""
301
- print("Fetching recent events from BIP...")
302
-
303
- load_dotenv(override=True)
304
- xsrf = os.getenv("XSRF_TOKEN", "")
305
- bip = os.getenv("BIP_SESSION", "")
306
-
307
- data, err = fetch_bip_events(xsrf, bip, page=1)
308
- if err:
309
- print(f"❌ Error: {err}")
310
- return
311
-
312
- resources = data.get("resources", [])
313
- if not resources:
314
- print("No events found.")
315
- return
316
-
317
- print(f"\nFound {len(resources)} recent events:")
318
- print("-" * 60)
319
- for res in resources:
320
- ev = parse_event(res)
321
- print(f"[{ev.get('id')}] {ev.get('event_code')} - {ev.get('event_name')} ({ev.get('status')})")
322
- print("-" * 60)
323
-
324
- def get_latest_event():
325
- """Fetches and prints only the single most recent event."""
326
- print("Fetching the latest event...")
327
-
328
- load_dotenv(override=True)
329
- xsrf = os.getenv("XSRF_TOKEN", "")
330
- bip = os.getenv("BIP_SESSION", "")
331
-
332
- data, err = fetch_bip_events(xsrf, bip, page=1)
333
- if err:
334
- print(f"❌ Error: {err}")
335
- return
336
-
337
- resources = data.get("resources", [])
338
- if not resources:
339
- print("No events found.")
340
- return
341
-
342
- ev = parse_event(resources[0])
343
- print("\n🌟 LATEST EVENT:")
344
- print("-" * 60)
345
- print(f"ID: {ev.get('id')}")
346
- print(f"Code: {ev.get('event_code')}")
347
- print(f"Name: {ev.get('event_name')}")
348
- print(f"Dates: {ev.get('start_date')} to {ev.get('end_date')}")
349
- print(f"Location: {ev.get('location')}")
350
- print(f"Status: {ev.get('status')}")
351
- print(f"Link: {ev.get('web_url')}")
352
- print("-" * 60)
353
-
354
- def test_telegram_alert():
355
- """Sends a dummy test message to the configured Telegram chat."""
356
- print("Sending test exact alert to Telegram...")
357
- success = send_telegram_message("🤖 <b>Test Alert from BIP CLI Notifier</b>\n\nYour Telegram integration is working perfectly!")
358
- if success:
359
- print(" Test message sent successfully!")
360
- else:
361
- print(" Failed to send test message. Check your .env configuration.")
362
-
363
- def start_loop():
364
- print("=" * 60)
365
- print("🚀 BIP CLI Notifier Started")
366
- print("=" * 60)
367
- print("Press Ctrl+C to stop the notifier at any time.\n")
368
-
369
- try:
370
- while True:
371
- process_tick()
372
-
373
- # Polling loop: Wait in exact intervals without stacking HTTP request delays
374
- target_time = time.time() + CHECK_INTERVAL_SECONDS
375
-
376
- while time.time() < target_time:
377
- cookies_updated = check_telegram_messages()
378
- if cookies_updated:
379
- # User sent new cookies, immediately break to run process_tick right now!
380
- break
381
-
382
- # Sleep safely for whatever is left over (up to 5 secs max per inner loop)
383
- remaining = target_time - time.time()
384
- if remaining > 0:
385
- time.sleep(min(5, remaining))
386
- except KeyboardInterrupt:
387
- print("\n\n🛑 Notifier manually stopped by user. Goodbye!")
388
-
389
-
390
- if __name__ == "__main__":
391
- parser = argparse.ArgumentParser(description="BIP Cloud Notifier CLI")
392
- parser.add_argument("--list-all", action="store_true", help="List all recent events and exit")
393
- parser.add_argument("--latest", action="store_true", help="Print details of the latest event and exit")
394
- parser.add_argument("--test-alert", action="store_true", help="Send a test message to Telegram and exit")
395
- parser.add_argument("--run", action="store_true", help="Start the continuous 1-minute monitoring loop")
396
-
397
- args = parser.parse_args()
398
-
399
- if args.list_all:
400
- list_all_events()
401
- elif args.latest:
402
- get_latest_event()
403
- elif args.test_alert:
404
- test_telegram_alert()
405
- elif args.run:
406
- start_loop()
407
- else:
408
- # If no arguments provided, default to starting the loop just like before
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  start_loop()
 
1
+ import os
2
+ import time
3
+ import argparse
4
+ import requests
5
+ from datetime import datetime
6
+ from dotenv import load_dotenv
7
+ import threading
8
+ from http.server import BaseHTTPRequestHandler, HTTPServer
9
+
10
+ # Load optional .env if present in same directory
11
+ load_dotenv()
12
+
13
+ # ==============================================================================
14
+ # CONFIGURATION
15
+ # ==============================================================================
16
+ # Environment values (Make sure to populate your .env file)
17
+ # We will reload these inside the loop so you can change them on the fly.
18
+
19
+ # Telegram settings (Optional, loaded from .env or hardcoded here)
20
+ TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
21
+ TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
22
+
23
+ # App check interval in seconds (default 60 secs = 1 min)
24
+ CHECK_INTERVAL_SECONDS = 60
25
+
26
+ BIP_API = "https://bip.bitsathy.ac.in/nova-api/student-activity-masters"
27
+ HEADERS = {
28
+ "accept": "application/json",
29
+ "x-requested-with": "XMLHttpRequest",
30
+ }
31
+
32
+ # State tracking for the loop
33
+ LAST_EVENT_ID = None
34
+ LAST_EVENT_CODE = None
35
+ SESSION_EXPIRED = False
36
+ EXPIRED_XSRF = None
37
+ EXPIRED_BIP = None
38
+
39
+
40
+ # ==============================================================================
41
+ # TELEGRAM NOTIFIER
42
+ # ==============================================================================
43
+ TELEGRAM_OFFSET = None
44
+
45
+ def update_env_file(new_xsrf, new_bip):
46
+ env_path = ".env"
47
+ if not os.path.exists(env_path):
48
+ with open(env_path, "w") as f:
49
+ if new_xsrf: f.write(f'XSRF_TOKEN="{new_xsrf}"\n')
50
+ if new_bip: f.write(f'BIP_SESSION="{new_bip}"\n')
51
+ return
52
+
53
+ with open(env_path, "r") as f:
54
+ lines = f.readlines()
55
+
56
+ with open(env_path, "w") as f:
57
+ xsrf_updated = False
58
+ bip_updated = False
59
+ for line in lines:
60
+ if line.startswith("XSRF_TOKEN=") and new_xsrf is not None:
61
+ f.write(f'XSRF_TOKEN="{new_xsrf}"\n')
62
+ xsrf_updated = True
63
+ elif line.startswith("BIP_SESSION=") and new_bip is not None:
64
+ f.write(f'BIP_SESSION="{new_bip}"\n')
65
+ bip_updated = True
66
+ else:
67
+ f.write(line)
68
+ if new_xsrf is not None and not xsrf_updated:
69
+ f.write(f'XSRF_TOKEN="{new_xsrf}"\n')
70
+ if new_bip is not None and not bip_updated:
71
+ f.write(f'BIP_SESSION="{new_bip}"\n')
72
+
73
+ def check_telegram_messages():
74
+ global TELEGRAM_OFFSET, SESSION_EXPIRED, EXPIRED_XSRF, EXPIRED_BIP
75
+ if not TELEGRAM_BOT_TOKEN: return False
76
+
77
+ url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/getUpdates"
78
+ params = {"timeout": 5}
79
+ if TELEGRAM_OFFSET:
80
+ params["offset"] = TELEGRAM_OFFSET
81
+
82
+ try:
83
+ r = requests.get(url, params=params, timeout=10)
84
+ data = r.json()
85
+ cookies_updated = False
86
+ if data.get("ok"):
87
+ for result in data.get("result", []):
88
+ TELEGRAM_OFFSET = result["update_id"] + 1
89
+ msg = result.get("message", {})
90
+ text = msg.get("text", "").strip()
91
+ chat_id = msg.get("chat", {}).get("id")
92
+
93
+ # Only process messages from our authorized user
94
+ if str(chat_id) == str(TELEGRAM_CHAT_ID):
95
+ new_xsrf = None
96
+ new_bip = None
97
+ for line in text.replace("\r", "\n").split("\n"):
98
+ line = line.strip()
99
+ if line.startswith("XSRF_TOKEN=") or line.startswith("XSRF-TOKEN="):
100
+ parts = line.split("=", 1)
101
+ if len(parts) > 1:
102
+ new_xsrf = parts[1].strip(' "\'')
103
+ elif line.startswith("BIP_SESSION=") or line.startswith("bip_session="):
104
+ parts = line.split("=", 1)
105
+ if len(parts) > 1:
106
+ new_bip = parts[1].strip(' "\'')
107
+
108
+ if new_xsrf or new_bip:
109
+ update_env_file(new_xsrf, new_bip)
110
+ send_telegram_message("✅ <b>Cookies Received!</b>\n\nI have updated the <code>.env</code> file and will now resume checking the BIP portal.")
111
+ SESSION_EXPIRED = False
112
+ EXPIRED_XSRF = None
113
+ EXPIRED_BIP = None
114
+ cookies_updated = True
115
+ print(f"[{datetime.now().strftime('%I:%M:%S %p')}] 🔄 Cookies received via Telegram! Resuming checks...")
116
+ return cookies_updated
117
+ except Exception as e:
118
+ return False
119
+
120
+ def send_telegram_message(text: str):
121
+ if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
122
+ print("\n[!] Telegram not configured. The following alert would have been sent:\n")
123
+ print(text)
124
+ print("-" * 50)
125
+ return False
126
+
127
+ url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
128
+ payload = {
129
+ "chat_id": TELEGRAM_CHAT_ID,
130
+ "text": text,
131
+ "parse_mode": "HTML",
132
+ "disable_web_page_preview": True
133
+ }
134
+
135
+ try:
136
+ r = requests.post(url, json=payload, timeout=10)
137
+ r.raise_for_status()
138
+ return True
139
+ except Exception as e:
140
+ print(f"[!] Failed to send telegram message: {e}")
141
+ return False
142
+
143
+ def send_event_alerts(events):
144
+ if not events:
145
+ return
146
+
147
+ msg = f"📢 <b>{len(events)} New BIP Event Found!</b>\n\n"
148
+
149
+ for i, ev in enumerate(events, 1):
150
+ # Format start date
151
+ start = ev.get('start_date', '')
152
+ if start:
153
+ start = datetime.strptime(start, "%Y-%m-%d").strftime("%d-%m-%Y")
154
+
155
+ # Format end date
156
+ end = ev.get('end_date', '')
157
+ if end:
158
+ end = datetime.strptime(end, "%Y-%m-%d").strftime("%d-%m-%Y")
159
+
160
+ msg += (
161
+ f"<b>{ev.get('event_code', '-')} - "
162
+ f"{ev.get('event_name', 'Unknown')}</b>\n\n"
163
+ f"{ev.get('event_category', '-')}\n\n"
164
+ f"{start} to {end}\n\n"
165
+ f"{ev.get('location', '-')}\n\n"
166
+ f"{ev.get('status', '-')}\n\n"
167
+ f"Seats: Max {ev.get('maximum_count', '-')} | "
168
+ f"Applied {ev.get('applied_count', '-')}\n"
169
+ f"<a href='{ev.get('web_url', '#')}'>View Event Here</a>\n"
170
+ f"────────────────\n"
171
+ )
172
+ send_telegram_message(msg)
173
+
174
+
175
+ # ==============================================================================
176
+ # SCRAPER LOGIC
177
+ # ==============================================================================
178
+ def fetch_bip_events(xsrf_token, bip_session, page=1):
179
+ cookies = {
180
+ "XSRF-TOKEN": xsrf_token,
181
+ "bip_session": bip_session
182
+ }
183
+ params = {"perPage": 10, "page": page}
184
+ try:
185
+ r = requests.get(BIP_API, params=params, headers=HEADERS, cookies=cookies, timeout=20)
186
+ # Check if session expired based on HTML redirect instead of JSON
187
+ if "text/html" in r.headers.get("Content-Type", "") or r.status_code == 401 or r.status_code == 403:
188
+ return None, "Session expired or invalid cookies."
189
+ r.raise_for_status()
190
+ return r.json(), None
191
+ except Exception as e:
192
+ return None, str(e)
193
+
194
+ def parse_event(resource):
195
+ data = {}
196
+ for f in resource.get("fields", []):
197
+ key = f.get("attribute")
198
+ val = f.get("value")
199
+ if key:
200
+ data[key] = val
201
+ data["id"] = resource["id"]["value"]
202
+ return data
203
+
204
+ def check_new_events(last_id, xsrf_token, bip_session):
205
+ """Fetches events and returns any newer than last_id. Automatically paginates if needed."""
206
+ new_events = []
207
+ page = 1
208
+
209
+ while page <= 10: # Limit to 10 pages for safety
210
+ data, err = fetch_bip_events(xsrf_token, bip_session, page)
211
+ if err or not data:
212
+ return None, err
213
+
214
+ resources = data.get("resources", [])
215
+ if not resources:
216
+ break
217
+
218
+ for res in resources:
219
+ ev = parse_event(res)
220
+ # Stop if we reach the last known new event
221
+ if last_id and str(ev["id"]) == str(last_id):
222
+ return new_events, None
223
+
224
+ new_events.append(ev)
225
+
226
+ # First-ever run scenario: just return the latest event to set the initial ID and avoid sending 100s of alerts
227
+ if not last_id and new_events:
228
+ return [new_events[0]], None
229
+
230
+ page += 1
231
+
232
+ return new_events, None
233
+
234
+
235
+ # ==============================================================================
236
+ # SCHEDULER ENGINE
237
+ # ==============================================================================
238
+ def process_tick():
239
+ global LAST_EVENT_ID, LAST_EVENT_CODE, SESSION_EXPIRED, EXPIRED_XSRF, EXPIRED_BIP
240
+ # Reload environment variables on every tick so user can update .env without restarting
241
+ load_dotenv(override=True)
242
+ xsrf = os.getenv("XSRF_TOKEN", "")
243
+ bip = os.getenv("BIP_SESSION", "")
244
+
245
+ now = datetime.now()
246
+ time_str = now.strftime('%I:%M:%S %p')
247
+
248
+ # Optional logic: only run between 8AM and 6PM
249
+ if now.hour < 8 or now.hour >= 18:
250
+ print(f"[{time_str}] 🌙 Out of hours (8 AM - 6 PM). Skipping check.")
251
+ return
252
+
253
+ if not xsrf or not bip:
254
+ print(f"[{time_str}] ⏳ Skipping check: Please configure XSRF_TOKEN and BIP_SESSION in the .env file.")
255
+ return
256
+
257
+ # Unsilenced print block to report resuming checking!
258
+ if SESSION_EXPIRED:
259
+ if xsrf == EXPIRED_XSRF and bip == EXPIRED_BIP:
260
+ print(f"{time_str.lower()} - ⏸️ Paused: Waiting for new cookies in .env to resume...")
261
+ return
262
+ else:
263
+ print(f"{time_str.lower()} - 🔄 New cookies detected! Resuming checks...")
264
+ SESSION_EXPIRED = False
265
+ EXPIRED_XSRF = None
266
+ EXPIRED_BIP = None
267
+
268
+ new_events, err = check_new_events(LAST_EVENT_ID, xsrf, bip)
269
+
270
+ if err:
271
+ print(f"{time_str.lower()} - Error scraping events: {err}")
272
+ send_telegram_message(
273
+ "⚠️ <b>Scraper Error!</b>\n\n"
274
+ f"The notifier encountered an error and has paused checking. Error:\n"
275
+ f"<code>{err}</code>\n\n"
276
+ "Please reply directly to this bot with your new cookies in this format to resume:\n\n"
277
+ "<code>XSRF_TOKEN=your_new_token_here\nBIP_SESSION=your_new_session_here</code>"
278
+ )
279
+ SESSION_EXPIRED = True
280
+ EXPIRED_XSRF = xsrf
281
+ EXPIRED_BIP = bip
282
+ return
283
+
284
+ if new_events:
285
+ # If LAST_EVENT_ID is None, it's the very first startup run. Set ID without alerting.
286
+ if LAST_EVENT_ID is None:
287
+ LAST_EVENT_ID = new_events[0]["id"]
288
+ LAST_EVENT_CODE = new_events[0].get('event_code', LAST_EVENT_ID)
289
+ print(f"{time_str.lower()} - EVENT ID : {LAST_EVENT_CODE} (Tracking started)")
290
+ else:
291
+ send_event_alerts(new_events)
292
+ LAST_EVENT_ID = new_events[0]["id"]
293
+ LAST_EVENT_CODE = new_events[0].get('event_code', LAST_EVENT_ID)
294
+ for ev in new_events:
295
+ code = ev.get('event_code', ev['id'])
296
+ print(f"{time_str.lower()} - 🚨 NEW EVENT ID : {code} (Alert Sent!)")
297
+ else:
298
+ # Just print the tracking status format exactly as requested on every 1-minute tick
299
+ print(f"{time_str.lower()} - EVENT ID : {LAST_EVENT_CODE}")
300
+
301
+ def list_all_events():
302
+ """Fetches the first page of events from BIP and prints them."""
303
+ print("Fetching recent events from BIP...")
304
+
305
+ load_dotenv(override=True)
306
+ xsrf = os.getenv("XSRF_TOKEN", "")
307
+ bip = os.getenv("BIP_SESSION", "")
308
+
309
+ data, err = fetch_bip_events(xsrf, bip, page=1)
310
+ if err:
311
+ print(f"❌ Error: {err}")
312
+ return
313
+
314
+ resources = data.get("resources", [])
315
+ if not resources:
316
+ print("No events found.")
317
+ return
318
+
319
+ print(f"\nFound {len(resources)} recent events:")
320
+ print("-" * 60)
321
+ for res in resources:
322
+ ev = parse_event(res)
323
+ print(f"[{ev.get('id')}] {ev.get('event_code')} - {ev.get('event_name')} ({ev.get('status')})")
324
+ print("-" * 60)
325
+
326
+ def get_latest_event():
327
+ """Fetches and prints only the single most recent event."""
328
+ print("Fetching the latest event...")
329
+
330
+ load_dotenv(override=True)
331
+ xsrf = os.getenv("XSRF_TOKEN", "")
332
+ bip = os.getenv("BIP_SESSION", "")
333
+
334
+ data, err = fetch_bip_events(xsrf, bip, page=1)
335
+ if err:
336
+ print(f"❌ Error: {err}")
337
+ return
338
+
339
+ resources = data.get("resources", [])
340
+ if not resources:
341
+ print("No events found.")
342
+ return
343
+
344
+ ev = parse_event(resources[0])
345
+ print("\n🌟 LATEST EVENT:")
346
+ print("-" * 60)
347
+ print(f"ID: {ev.get('id')}")
348
+ print(f"Code: {ev.get('event_code')}")
349
+ print(f"Name: {ev.get('event_name')}")
350
+ print(f"Dates: {ev.get('start_date')} to {ev.get('end_date')}")
351
+ print(f"Location: {ev.get('location')}")
352
+ print(f"Status: {ev.get('status')}")
353
+ print(f"Link: {ev.get('web_url')}")
354
+ print("-" * 60)
355
+
356
+ def test_telegram_alert():
357
+ """Sends a dummy test message to the configured Telegram chat."""
358
+ print("Sending test exact alert to Telegram...")
359
+ success = send_telegram_message("🤖 <b>Test Alert from BIP CLI Notifier</b>\n\nYour Telegram integration is working perfectly!")
360
+ if success:
361
+ print(" Test message sent successfully!")
362
+ else:
363
+ print("❌ Failed to send test message. Check your .env configuration.")
364
+
365
+ def start_loop():
366
+ print("=" * 60)
367
+ print("🚀 BIP CLI Notifier Started")
368
+ print("=" * 60)
369
+ print("Press Ctrl+C to stop the notifier at any time.\n")
370
+
371
+ try:
372
+ while True:
373
+ process_tick()
374
+
375
+ # Polling loop: Wait in exact intervals without stacking HTTP request delays
376
+ target_time = time.time() + CHECK_INTERVAL_SECONDS
377
+
378
+ while time.time() < target_time:
379
+ cookies_updated = check_telegram_messages()
380
+ if cookies_updated:
381
+ # User sent new cookies, immediately break to run process_tick right now!
382
+ break
383
+
384
+ # Sleep safely for whatever is left over (up to 5 secs max per inner loop)
385
+ remaining = target_time - time.time()
386
+ if remaining > 0:
387
+ time.sleep(min(5, remaining))
388
+ except KeyboardInterrupt:
389
+ print("\n\n🛑 Notifier manually stopped by user. Goodbye!")
390
+
391
+
392
+ def run_dummy_server():
393
+ """
394
+ Hugging Face Spaces requires a web server listening on port 7860 to pass health checks.
395
+ This tiny server does nothing but say "I am alive" in the background.
396
+ """
397
+ class DummyHandler(BaseHTTPRequestHandler):
398
+ def do_GET(self):
399
+ self.send_response(200)
400
+ self.send_header('Content-type', 'text/html')
401
+ self.end_headers()
402
+ self.wfile.write(b"BIP Notifier is running in the background!")
403
+
404
+ def log_message(self, format, *args):
405
+ pass # Keep terminal clean from HTTP logs
406
+
407
+ server = HTTPServer(('0.0.0.0', 7860), DummyHandler)
408
+ server.serve_forever()
409
+
410
+
411
+ if __name__ == "__main__":
412
+ print("Starting BIP CLI Notifier...")
413
+ parser = argparse.ArgumentParser(description="BIP Cloud Notifier CLI")
414
+ parser.add_argument("--list-all", action="store_true", help="List all recent events and exit")
415
+ parser.add_argument("--latest", action="store_true", help="Print details of the latest event and exit")
416
+ parser.add_argument("--test-alert", action="store_true", help="Send a test message to Telegram and exit")
417
+ parser.add_argument("--run", action="store_true", help="Start the continuous 1-minute monitoring loop")
418
+ print("Parsed arguments:", parser.parse_args())
419
+ args = parser.parse_args()
420
+
421
+ if args.list_all:
422
+ list_all_events()
423
+ elif args.latest:
424
+ get_latest_event()
425
+ elif args.test_alert:
426
+ test_telegram_alert()
427
+ elif args.run:
428
+ threading.Thread(target=run_dummy_server, daemon=True).start()
429
+ start_loop()
430
+ else:
431
+ # If no arguments provided, default to starting the loop just like before
432
+ threading.Thread(target=run_dummy_server, daemon=True).start()
433
  start_loop()