DinoPLayZ commited on
Commit
9f380b0
Β·
verified Β·
1 Parent(s): fdc6a90

Uploaded the Main Python files

Browse files
Files changed (3) hide show
  1. Dockerfile +31 -0
  2. main.py +409 -0
  3. requirements.txt +2 -0
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use a lightweight Python base image
2
+ FROM python:3.10-slim
3
+
4
+ # Set up a working directory
5
+ WORKDIR /app
6
+
7
+ # In HuggingFace Spaces it's crucial to set up the correct user privileges
8
+ # Spaces require running as a non-root user with UID 1000
9
+ RUN useradd -m -u 1000 user
10
+ USER user
11
+
12
+ # Ensure the /app path is owned by our new user
13
+ # (This step must run as root before we switch, so we move it up)
14
+ USER root
15
+ RUN chown -R user:user /app
16
+ USER user
17
+
18
+ # Set environmental variables
19
+ ENV PATH="/home/user/.local/bin:$PATH"
20
+
21
+ # Copy in the requirements file and install dependencies
22
+ COPY --chown=user:user requirements.txt .
23
+ RUN pip install --no-cache-dir -r requirements.txt
24
+
25
+ # Copy the actual Python script and .env if it exists
26
+ COPY --chown=user:user main.py .
27
+ # We don't forcibly COPY .env because users should use HF Secrets,
28
+ # but if it's there it won't break anything.
29
+
30
+ # The command to start our infinite loop script
31
+ CMD ["python", "main.py", "--run"]
main.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ requests
2
+ python-dotenv