AstraOS commited on
Commit
cfd7b39
Β·
verified Β·
1 Parent(s): 75805c6

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +902 -0
app.py ADDED
@@ -0,0 +1,902 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import filesystem
2
+ import json
3
+ import time
4
+ import threading
5
+ import logging
6
+ import telebot
7
+ import os
8
+ from fastapi import FastAPI
9
+
10
+ import fs
11
+ from fs import path
12
+
13
+
14
+ # filesystem = fs.open_fs("mem://") # Use "mem://" for in-memory
15
+
16
+ # # ------------------------------------------------------------------------------
17
+ # # Directories and File Paths
18
+ # # ------------------------------------------------------------------------------
19
+
20
+ # BOT_INFO_DIR = "bot_info"
21
+ # CHAT_LOGS_DIR = path.join(BOT_INFO_DIR, "chat_logs")
22
+ # SUBSCRIBERS_FILE = path.join(BOT_INFO_DIR, "subscribers.json")
23
+ # UNSUBSCRIBE_REQUESTS_FILE = path.join(BOT_INFO_DIR, "request_unsubscribe.json")
24
+ # LOG_FILE = path.join(BOT_INFO_DIR, "bot.log")
25
+
26
+ # # Create required directories if not present.
27
+ # for directory in [BOT_INFO_DIR, CHAT_LOGS_DIR]:
28
+ # if not filesystem.exists(directory):
29
+ # try:
30
+ # filesystem.makedirs(directory)
31
+ # print(f"Created directory: {directory}")
32
+ # except Exception as e:
33
+ # print(f"Error creating directory {directory}: {e}")
34
+
35
+
36
+
37
+ from fs import open_fs # pip install fs
38
+ from fs.errors import ResourceError
39
+ import json
40
+
41
+ # -------------------------------------------------------------------
42
+ # Pick the filesystem you want to use.
43
+ # β€’ "mem://" β†’ in‑memory (discarded when the process exits)
44
+ # β€’ "osfs://." β†’ the real working‑directory on disk
45
+ # β€’ "zip://bot_bundle.zip" β†’ a zip file you can ship around
46
+ # -------------------------------------------------------------------
47
+ filesystem = open_fs("mem://") # ⭐ change me if you want another backend
48
+
49
+ # Logical paths inside that filesystem
50
+ BOT_INFO_DIR = "bot_info"
51
+ CHAT_LOGS_DIR = f"{BOT_INFO_DIR}/chat_logs"
52
+ SUBSCRIBERS_FILE = f"{BOT_INFO_DIR}/subscribers.json"
53
+ UNSUBSCRIBE_REQUESTS_FILE = f"{BOT_INFO_DIR}/request_unsubscribe.json"
54
+ LOG_FILE = f"{BOT_INFO_DIR}/bot.log"
55
+
56
+ # -------------------------------------------------------------------
57
+ # Create the directory tree (recreate=True β‡’ no error if it exists)
58
+ # -------------------------------------------------------------------
59
+ filesystem.makedirs(CHAT_LOGS_DIR, recreate=True)
60
+
61
+ # -------------------------------------------------------------------
62
+ # Touch the files if they don’t exist yet
63
+ # (You can skip this if you plan to open() them in 'a' or 'w' mode later)
64
+ # -------------------------------------------------------------------
65
+ for path, initial in [
66
+ (SUBSCRIBERS_FILE, []), # start with an empty dict
67
+ (UNSUBSCRIBE_REQUESTS_FILE, []), # start with an empty list
68
+ (LOG_FILE, ""), # plain‑text log
69
+ ]:
70
+ if not filesystem.exists(path):
71
+ with filesystem.open(path, "w") as fh:
72
+ # Serialise JSON files, or just write a blank string for the log
73
+ if path.endswith(".json"):
74
+ json.dump(initial, fh, indent=2)
75
+ else:
76
+ fh.write(str(initial))
77
+
78
+ print("Filesystem contents:")
79
+ print(filesystem.tree()) # pretty‑print the virtual tree
80
+
81
+
82
+ # ------------------------------------------------------------------------------
83
+ # Logging Configuration: Console and File Logging
84
+ # ------------------------------------------------------------------------------
85
+
86
+ # logging.basicConfig(
87
+ # level=logging.DEBUG,
88
+ # format='%(asctime)s [%(levelname)s] %(message)s',
89
+ # datefmt='%Y-%m-%d %H:%M:%S'
90
+ # )
91
+ # try:
92
+ # file_handler = logging.FileHandler(LOG_FILE)
93
+ # file_handler.setLevel(logging.DEBUG)
94
+ # formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
95
+ # file_handler.setFormatter(formatter)
96
+ # logging.getLogger().addHandler(file_handler)
97
+ # except Exception as e:
98
+ # logging.error(f"Error setting up file logging: {e}")
99
+
100
+
101
+
102
+ # ----------------------------------------------------------------------
103
+ # Logging Configuration β†’ Smart Auto-Switching Handler (mem:// realtime)
104
+ # ----------------------------------------------------------------------
105
+
106
+ import logging
107
+ import time
108
+ from collections import deque
109
+ from fs import path as fspath
110
+
111
+ class SmartFSLogHandler(logging.Handler):
112
+ """
113
+ Logs to a PyFilesystem file (e.g., mem:// or osfs).
114
+ Automatically switches between open-per-record and keep-open mode
115
+ based on log frequency (adaptive throttling).
116
+ """
117
+ def __init__(self, fs, file_path, rate_window=5, rate_threshold=10):
118
+ super().__init__()
119
+ self.fs = fs
120
+ self.file_path = file_path
121
+ self._timestamps = deque(maxlen=rate_window * rate_threshold * 2)
122
+ self._fh = None
123
+ self.rate_window = rate_window
124
+ self.rate_threshold = rate_threshold
125
+ self._last_check = 0
126
+
127
+ parent = fspath.dirname(file_path)
128
+ fs.makedirs(parent, recreate=True)
129
+
130
+ def emit(self, record):
131
+ try:
132
+ msg = self.format(record) + "\n"
133
+ now = time.time()
134
+ self._timestamps.append(now)
135
+
136
+ # Every second, re-evaluate mode
137
+ if now - self._last_check > 1:
138
+ self._adjust_mode()
139
+ self._last_check = now
140
+
141
+ if self._fh:
142
+ self._fh.write(msg)
143
+ self._fh.flush()
144
+ else:
145
+ with self.fs.open(self.file_path, "a") as fh:
146
+ fh.write(msg)
147
+
148
+ except Exception:
149
+ self.handleError(record)
150
+
151
+ def _adjust_mode(self):
152
+ now = time.time()
153
+ recent = [ts for ts in self._timestamps if now - ts <= self.rate_window]
154
+ rate = len(recent) / self.rate_window
155
+
156
+ if rate >= self.rate_threshold and self._fh is None:
157
+ self._fh = self.fs.open(self.file_path, "a")
158
+ elif rate < self.rate_threshold and self._fh:
159
+ self._fh.close()
160
+ self._fh = None
161
+
162
+ def close(self):
163
+ if self._fh:
164
+ try:
165
+ self._fh.close()
166
+ finally:
167
+ self._fh = None
168
+ super().close()
169
+
170
+ # ─── Apply smart log handler ─────────────────────────────────────────
171
+
172
+ log_format = "%(asctime)s [%(levelname)s] %(message)s"
173
+ log_datefmt = "%Y-%m-%d %H:%M:%S"
174
+
175
+ logging.basicConfig(
176
+ level=logging.DEBUG,
177
+ format=log_format,
178
+ datefmt=log_datefmt,
179
+ handlers=[
180
+ SmartFSLogHandler(filesystem, LOG_FILE, rate_window=5, rate_threshold=10),
181
+ logging.StreamHandler() # Console output (optional)
182
+ ],
183
+ )
184
+
185
+
186
+ # ------------------------------------------------------------------------------
187
+ # Bot Initialization and Owner Configuration
188
+ # ------------------------------------------------------------------------------
189
+
190
+ TOKEN = os.environ["BOT_TOKEN"]
191
+ bot = telebot.TeleBot(TOKEN)
192
+ OWNER_ID = os.environ["CHAT_IDs"] # Replace with your Telegram user ID (owner-only commands).
193
+
194
+ # ------------------------------------------------------------------------------
195
+ # Utility Functions: Subscribers, Unsubscribe Requests, and Chat Logging
196
+ # ------------------------------------------------------------------------------
197
+
198
+ def load_subscribers():
199
+ """Load subscribers from file; create if missing."""
200
+ if not filesystem.exists(SUBSCRIBERS_FILE):
201
+ logging.info(f"{SUBSCRIBERS_FILE} not found. Creating a new one.")
202
+ save_subscribers([])
203
+ return []
204
+ try:
205
+ with filesystem.open(SUBSCRIBERS_FILE, "r") as f:
206
+ subscribers = json.load(f)
207
+ logging.debug(f"Loaded subscribers: {subscribers}")
208
+ return subscribers
209
+ except Exception as e:
210
+ logging.error(f"Error loading subscribers: {e}")
211
+ return []
212
+
213
+ def save_subscribers(subscribers):
214
+ """Save the subscribers list to file."""
215
+ try:
216
+ with filesystem.open(SUBSCRIBERS_FILE, "w") as f:
217
+ json.dump(subscribers, f, indent=4)
218
+ logging.debug("Saved subscribers.")
219
+ except Exception as e:
220
+ logging.error(f"Error saving subscribers: {e}")
221
+
222
+ def load_unsubscribe_requests():
223
+ """Load unsubscribe requests from file; create if missing."""
224
+ if not filesystem.exists(UNSUBSCRIBE_REQUESTS_FILE):
225
+ save_unsubscribe_requests([])
226
+ return []
227
+ try:
228
+ with filesystem.open(UNSUBSCRIBE_REQUESTS_FILE, "r") as f:
229
+ reqs = json.load(f)
230
+ return reqs
231
+ except Exception as e:
232
+ logging.error(f"Error loading unsubscribe requests: {e}")
233
+ return []
234
+
235
+ def save_unsubscribe_requests(reqs):
236
+ """Save unsubscribe requests to file."""
237
+ try:
238
+ with filesystem.open(UNSUBSCRIBE_REQUESTS_FILE, "w") as f:
239
+ json.dump(reqs, f, indent=4)
240
+ logging.debug("Saved unsubscribe requests.")
241
+ except Exception as e:
242
+ logging.error(f"Error saving unsubscribe requests: {e}")
243
+
244
+ def log_chat_message(message):
245
+ """Append each incoming message (in JSON Lines format) to its chat log file."""
246
+ chat_id = message.chat.id
247
+ log_file_path = path.join(CHAT_LOGS_DIR, f"{chat_id}.log")
248
+ log_entry = {
249
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
250
+ "message_id": message.message_id,
251
+ "user_id": message.from_user.id if message.from_user else None,
252
+ "username": message.from_user.username if message.from_user else None,
253
+ "first_name": message.from_user.first_name if message.from_user else None,
254
+ "last_name": message.from_user.last_name if message.from_user else None,
255
+ "text": message.text,
256
+ "chat_id": chat_id,
257
+ "chat_type": message.chat.type
258
+ }
259
+ try:
260
+ with filesystem.open(log_file_path, "a") as f:
261
+ f.write(json.dumps(log_entry) + "\n")
262
+ logging.debug(f"Logged message for chat {chat_id}.")
263
+ except Exception as e:
264
+ logging.error(f"Error logging message for chat {chat_id}: {e}")
265
+
266
+ def ensure_subscriber(message):
267
+ """
268
+ Automatically add the user to subscribers if not already present.
269
+ Each record contains basic info plus an 'unsubscribe_requested' flag.
270
+ """
271
+ chat_id = message.chat.id
272
+ subscribers = load_subscribers()
273
+ if not any(sub.get("chat_id") == chat_id for sub in subscribers):
274
+ new_sub = {
275
+ "chat_id": chat_id,
276
+ "user_id": message.from_user.id if message.from_user else None,
277
+ "username": message.from_user.username if message.from_user else "",
278
+ "first_name": message.from_user.first_name if message.from_user else "",
279
+ "last_name": message.from_user.last_name if message.from_user else "",
280
+ "subscription_date": time.strftime("%Y-%m-%d %H:%M:%S"),
281
+ "unsubscribe_requested": False
282
+ }
283
+ subscribers.append(new_sub)
284
+ save_subscribers(subscribers)
285
+ logging.info(f"Auto-added subscriber: {new_sub}")
286
+
287
+ # ------------------------------------------------------------------------------
288
+ # Update Listener: Log Raw Updates and Auto-Subscribe
289
+ # ------------------------------------------------------------------------------
290
+
291
+ def update_listener(updates):
292
+ """Log raw update data and auto-subscribe users from incoming messages."""
293
+ for update in updates:
294
+ try:
295
+ update_data = update.to_dict()
296
+ except Exception:
297
+ update_data = update.__dict__
298
+ logging.debug(f"Received update: {update_data}")
299
+ # Auto-subscribe if update contains a message.
300
+ if hasattr(update, "message") and update.message is not None:
301
+ ensure_subscriber(update.message)
302
+
303
+ bot.set_update_listener(update_listener)
304
+
305
+ # ------------------------------------------------------------------------------
306
+ # Retrieve Bot Information
307
+ # ------------------------------------------------------------------------------
308
+
309
+ try:
310
+ bot_info = bot.get_me()
311
+ logging.info(f"Bot info: id={bot_info.id}, username={bot_info.username}")
312
+ except Exception as e:
313
+ logging.error(f"Error retrieving bot info: {e}")
314
+ raise
315
+
316
+ # ------------------------------------------------------------------------------
317
+ # Command Handlers (All call ensure_subscriber explicitly)
318
+ # ------------------------------------------------------------------------------
319
+
320
+ @bot.message_handler(commands=['start', 'help'])
321
+ def send_help(message):
322
+ """Send a help message with detailed usage instructions for every command."""
323
+ ensure_subscriber(message)
324
+ if message.from_user.id == OWNER_ID:
325
+ help_text = (
326
+ "<b>Welcome to the Alert Bot! πŸ˜ƒ</b>\n\n"
327
+ "<b>User Commands:</b>\n"
328
+ "β€’ <b>/subscribe</b> - Confirm your subscription or re‑subscribe (cancels any pending unsubscribe request).\n"
329
+ "β€’ <b>/unsubscribe</b> - Request to unsubscribe (your request will be pending approval).\n"
330
+ "β€’ <b>/status</b> - Check your current subscription status.\n"
331
+ "β€’ <b>/myinfo</b> - View your subscription details.\n\n"
332
+ "<b>Owner‑Only Commands:</b>\n"
333
+ "β€’ <b>/broadcast &lt;message&gt;</b> - <i>Send an alert</i> to all subscribers.\n"
334
+ " <i>Example:</i> <code>/broadcast Attention: Server maintenance at 10PM.</code>\n"
335
+ "β€’ <b>/stats</b> - View subscription statistics.\n"
336
+ "β€’ <b>/list_subscribers</b> - List all subscribers with detailed info (shows only User ID).\n"
337
+ "β€’ <b>/list_unsubscribes</b> - List all pending unsubscribe requests (shows only User ID).\n"
338
+ "β€’ <b>/process_unsubscribes &lt;approve|deny&gt; &lt;all|numbers&gt;</b> - Process unsubscribe requests.\n"
339
+ " <i>Examples:</i>\n"
340
+ " β€’ <code>/process_unsubscribes approve all</code>\n"
341
+ " β€’ <code>/process_unsubscribes deny 2 4</code>\n"
342
+ "β€’ <b>/clear_chat_logs &lt;chat_id|all&gt;</b> - Clear chat log(s).\n"
343
+ " <i>Examples:</i>\n"
344
+ " β€’ <code>/clear_chat_logs all</code>\n"
345
+ " β€’ <code>/clear_chat_logs 123456789</code>\n"
346
+ "β€’ <b>/ping</b> - Check if the bot is responsive.\n"
347
+ )
348
+ else:
349
+ help_text = (
350
+ "<b>Welcome to the Alert Bot! πŸ˜ƒ</b>\n\n"
351
+ "<b>User Commands:</b>\n"
352
+ "β€’ <b>/subscribe</b> - Confirm your subscription or re‑subscribe.\n"
353
+ "β€’ <b>/unsubscribe</b> - Request to unsubscribe (your request is pending approval).\n"
354
+ "β€’ <b>/status</b> - Check your current subscription status.\n"
355
+ "β€’ <b>/myinfo</b> - View your subscription details.\n"
356
+ "β€’ <b>/ping</b> - Check if the bot is responsive.\n"
357
+ )
358
+ try:
359
+ bot.send_message(message.chat.id, help_text, parse_mode="HTML")
360
+ logging.info(f"Sent help message to chat {message.chat.id}.")
361
+ except Exception as e:
362
+ logging.error(f"Error sending help message: {e}")
363
+
364
+ @bot.message_handler(commands=['subscribe'])
365
+ def subscribe(message):
366
+ """(Re)confirm a user's subscription and auto-subscribe if needed."""
367
+ ensure_subscriber(message)
368
+ chat_id = message.chat.id
369
+ subscribers = load_subscribers()
370
+ found = False
371
+ for sub in subscribers:
372
+ if sub.get("chat_id") == chat_id:
373
+ found = True
374
+ if sub.get("unsubscribe_requested"):
375
+ sub["unsubscribe_requested"] = False
376
+ save_subscribers(subscribers)
377
+ reqs = load_unsubscribe_requests()
378
+ new_reqs = [req for req in reqs if req.get("chat_id") != chat_id]
379
+ if len(new_reqs) < len(reqs):
380
+ save_unsubscribe_requests(new_reqs)
381
+ try:
382
+ bot.send_message(chat_id, "βœ… <b>Your subscription has been re‑confirmed! πŸŽ‰</b>", parse_mode="HTML")
383
+ logging.info(f"Subscription re‑confirmed for chat {chat_id}.")
384
+ except Exception as e:
385
+ logging.error(f"Error sending re‑confirmation: {e}")
386
+ else:
387
+ try:
388
+ bot.send_message(chat_id, "πŸ˜ƒ <b>You are already subscribed!</b>", parse_mode="HTML")
389
+ logging.info(f"Chat {chat_id} is already subscribed.")
390
+ except Exception as e:
391
+ logging.error(f"Error sending subscription message: {e}")
392
+ break
393
+ if not found:
394
+ new_sub = {
395
+ "chat_id": chat_id,
396
+ "user_id": message.from_user.id if message.from_user else None,
397
+ "username": message.from_user.username if message.from_user else "",
398
+ "first_name": message.from_user.first_name if message.from_user else "",
399
+ "last_name": message.from_user.last_name if message.from_user else "",
400
+ "subscription_date": time.strftime("%Y-%m-%d %H:%M:%S"),
401
+ "unsubscribe_requested": False
402
+ }
403
+ subscribers.append(new_sub)
404
+ save_subscribers(subscribers)
405
+ try:
406
+ bot.send_message(chat_id, "βœ… <b>You have been subscribed to alerts! πŸŽ‰</b>", parse_mode="HTML")
407
+ logging.info(f"New subscriber added: {new_sub}")
408
+ except Exception as e:
409
+ logging.error(f"Error sending subscription confirmation: {e}")
410
+
411
+ @bot.message_handler(commands=['unsubscribe'])
412
+ def unsubscribe(message):
413
+ """Mark the user as having requested unsubscription and record the request."""
414
+ ensure_subscriber(message)
415
+ chat_id = message.chat.id
416
+ subscribers = load_subscribers()
417
+ found = False
418
+ for sub in subscribers:
419
+ if sub.get("chat_id") == chat_id:
420
+ if sub.get("unsubscribe_requested"):
421
+ try:
422
+ bot.send_message(chat_id, "⚠️ <b>You have already requested to unsubscribe.</b>", parse_mode="HTML")
423
+ except Exception as e:
424
+ logging.error(f"Error sending message: {e}")
425
+ return
426
+ else:
427
+ sub["unsubscribe_requested"] = True
428
+ found = True
429
+ break
430
+ if found:
431
+ save_subscribers(subscribers)
432
+ reqs = load_unsubscribe_requests()
433
+ if not any(req.get("chat_id") == chat_id for req in reqs):
434
+ new_req = {
435
+ "chat_id": chat_id,
436
+ "user_id": message.from_user.id if message.from_user else None,
437
+ "username": message.from_user.username if message.from_user else "",
438
+ "first_name": message.from_user.first_name if message.from_user else "",
439
+ "last_name": message.from_user.last_name if message.from_user else "",
440
+ "request_date": time.strftime("%Y-%m-%d %H:%M:%S")
441
+ }
442
+ reqs.append(new_req)
443
+ save_unsubscribe_requests(reqs)
444
+ try:
445
+ bot.send_message(chat_id,
446
+ "⚠️ <b>Your unsubscribe request has been noted.</b>\nYou will continue to receive alerts until approved.",
447
+ parse_mode="HTML")
448
+ logging.info(f"Unsubscribe request noted for chat {chat_id}.")
449
+ except Exception as e:
450
+ logging.error(f"Error sending unsubscribe confirmation: {e}")
451
+ else:
452
+ try:
453
+ bot.send_message(chat_id, "ℹ️ <b>You are not in our subscriber list yet!</b>", parse_mode="HTML")
454
+ except Exception as e:
455
+ logging.error(f"Error sending not-subscribed message: {e}")
456
+
457
+ @bot.message_handler(commands=['status'])
458
+ def status(message):
459
+ """Provide the subscription status of the user."""
460
+ ensure_subscriber(message)
461
+ chat_id = message.chat.id
462
+ subscribers = load_subscribers()
463
+ status_msg = "ℹ️ <b>You are not subscribed. Use /subscribe to subscribe.</b>"
464
+ for sub in subscribers:
465
+ if sub.get("chat_id") == chat_id:
466
+ if sub.get("unsubscribe_requested"):
467
+ status_msg = "⚠️ <b>You have requested to unsubscribe. Your request is pending approval.</b>"
468
+ else:
469
+ status_msg = "βœ… <b>You are subscribed to alerts.</b>"
470
+ break
471
+ try:
472
+ bot.send_message(chat_id, status_msg, parse_mode="HTML")
473
+ logging.info(f"Sent subscription status to chat {chat_id}.")
474
+ except Exception as e:
475
+ logging.error(f"Error sending status: {e}")
476
+
477
+ @bot.message_handler(commands=['myinfo'])
478
+ def myinfo(message):
479
+ """Send the stored subscriber info for the current user (showing only User ID)."""
480
+ ensure_subscriber(message)
481
+ chat_id = message.chat.id
482
+ subscribers = load_subscribers()
483
+ user_info = next((sub for sub in subscribers if sub.get("chat_id") == chat_id), None)
484
+ if user_info:
485
+ info_text = (
486
+ f"<b>Your Subscription Info:</b>\n"
487
+ f"β€’ <b>User ID:</b> {user_info.get('user_id')}\n"
488
+ f"β€’ <b>Username:</b> @{user_info.get('username')}\n"
489
+ f"β€’ <b>Name:</b> {user_info.get('first_name')} {user_info.get('last_name')}\n"
490
+ f"β€’ <b>Subscribed On:</b> {user_info.get('subscription_date')}\n"
491
+ f"β€’ <b>Unsubscribe Requested:</b> {user_info.get('unsubscribe_requested')}"
492
+ )
493
+ else:
494
+ info_text = "ℹ️ <b>You are not subscribed. Use /subscribe to subscribe.</b>"
495
+ try:
496
+ bot.send_message(chat_id, info_text, parse_mode="HTML")
497
+ logging.info(f"Sent subscription info to chat {chat_id}.")
498
+ except Exception as e:
499
+ logging.error(f"Error sending myinfo: {e}")
500
+
501
+ @bot.message_handler(commands=['broadcast'])
502
+ def broadcast(message):
503
+ """
504
+ Owner‑only command to broadcast an alert message.
505
+ <b>Usage:</b> <code>/broadcast &lt;message&gt;</code>
506
+ <i>Example:</i> <code>/broadcast Attention: Server maintenance at 10PM.</code>
507
+ """
508
+ ensure_subscriber(message)
509
+ if message.from_user.id != OWNER_ID:
510
+ try:
511
+ bot.send_message(message.chat.id, "🚫 <b>You are not authorized to broadcast alerts.</b>", parse_mode="HTML")
512
+ except Exception as e:
513
+ logging.error(f"Error sending unauthorized message: {e}")
514
+ return
515
+
516
+ broadcast_text = message.text[len('/broadcast'):].strip()
517
+ if not broadcast_text:
518
+ usage = (
519
+ "<b>Usage of /broadcast:</b>\n"
520
+ "β€’ <code>/broadcast &lt;message&gt;</code>\n\n"
521
+ "Example:\n"
522
+ "β€’ <code>/broadcast Attention: Server maintenance at 10PM.</code>"
523
+ )
524
+ try:
525
+ bot.send_message(message.chat.id, usage, parse_mode="HTML")
526
+ except Exception as e:
527
+ logging.error(f"Error sending broadcast usage guide: {e}")
528
+ return
529
+
530
+ subscribers = load_subscribers()
531
+ if not subscribers:
532
+ try:
533
+ bot.send_message(message.chat.id, "ℹ️ <b>No subscribers to send alert.</b>", parse_mode="HTML")
534
+ except Exception as e:
535
+ logging.error(f"Error sending no-subscriber message: {e}")
536
+ return
537
+
538
+ for sub in subscribers:
539
+ chat_id = sub.get("chat_id")
540
+ try:
541
+ response = bot.send_message(chat_id, broadcast_text, parse_mode="HTML")
542
+ logging.info(f"Broadcast sent to chat {chat_id} (message_id={response.message_id}).")
543
+ except Exception as e:
544
+ logging.error(f"Error sending broadcast to chat {chat_id}: {e}")
545
+ try:
546
+ bot.send_message(message.chat.id, "βœ… <b>Broadcast message sent to all subscribers.</b>", parse_mode="HTML")
547
+ except Exception as e:
548
+ logging.error(f"Error sending broadcast confirmation: {e}")
549
+
550
+ @bot.message_handler(commands=['stats'])
551
+ def stats(message):
552
+ """
553
+ Owner‑only command to display subscription statistics.
554
+ """
555
+ ensure_subscriber(message)
556
+ if message.from_user.id != OWNER_ID:
557
+ try:
558
+ bot.send_message(message.chat.id, "🚫 <b>You are not authorized to view stats.</b>", parse_mode="HTML")
559
+ except Exception as e:
560
+ logging.error(f"Error sending unauthorized stats message: {e}")
561
+ return
562
+ subscribers = load_subscribers()
563
+ total = len(subscribers)
564
+ active = len(subscribers)
565
+ stats_text = f"<b>Total Subscribers:</b> {total}\n<b>Active Subscribers:</b> {active}"
566
+ try:
567
+ bot.send_message(message.chat.id, stats_text, parse_mode="HTML")
568
+ logging.info(f"Sent stats to owner: {stats_text}")
569
+ except Exception as e:
570
+ logging.error(f"Error sending stats: {e}")
571
+
572
+ @bot.message_handler(commands=['list_subscribers'])
573
+ def list_subscribers(message):
574
+ """
575
+ Owner‑only command to list detailed subscriber info.
576
+ (Only <b>User ID</b> is shown.)
577
+ """
578
+ ensure_subscriber(message)
579
+ if message.from_user.id != OWNER_ID:
580
+ try:
581
+ bot.send_message(message.chat.id, "🚫 <b>You are not authorized to view subscribers.</b>", parse_mode="HTML")
582
+ except Exception as e:
583
+ logging.error(f"Error sending unauthorized list_subscribers message: {e}")
584
+ return
585
+ subscribers = load_subscribers()
586
+ if not subscribers:
587
+ reply = "ℹ️ <b>No subscribers found.</b>"
588
+ else:
589
+ reply = "<b>Subscribers:</b>\n"
590
+ for idx, sub in enumerate(subscribers, start=1):
591
+ reply += (
592
+ f"{idx}. <b>User ID:</b> {sub.get('user_id')}, "
593
+ f"<b>Name:</b> {sub.get('first_name')} {sub.get('last_name')}, "
594
+ f"<b>Username:</b> @{sub.get('username')}, "
595
+ f"<b>Subscribed On:</b> {sub.get('subscription_date')}, "
596
+ f"<b>Unsubscribe Requested:</b> {sub.get('unsubscribe_requested')}\n"
597
+ )
598
+ try:
599
+ bot.send_message(message.chat.id, reply, parse_mode="HTML")
600
+ logging.info("Sent detailed subscriber list to owner.")
601
+ except Exception as e:
602
+ logging.error(f"Error sending subscribers list: {e}")
603
+
604
+ @bot.message_handler(commands=['list_unsubscribes'])
605
+ def list_unsubscribes(message):
606
+ """
607
+ Owner‑only command to list all pending unsubscribe requests.
608
+ (Only <b>User ID</b> is shown.)
609
+ """
610
+ ensure_subscriber(message)
611
+ if message.from_user.id != OWNER_ID:
612
+ try:
613
+ bot.send_message(message.chat.id, "🚫 <b>You are not authorized to view unsubscribe requests.</b>", parse_mode="HTML")
614
+ except Exception as e:
615
+ logging.error(f"Error sending unauthorized list_unsubscribes message: {e}")
616
+ return
617
+ reqs = load_unsubscribe_requests()
618
+ if not reqs:
619
+ reply = "ℹ️ <b>No unsubscribe requests found.</b>"
620
+ else:
621
+ reply = "<b>Unsubscribe Requests:</b>\n"
622
+ for idx, req in enumerate(reqs, start=1):
623
+ reply += (
624
+ f"{idx}. <b>User ID:</b> {req.get('user_id')}, "
625
+ f"<b>Name:</b> {req.get('first_name')} {req.get('last_name')}, "
626
+ f"<b>Username:</b> @{req.get('username')}, "
627
+ f"<b>Requested On:</b> {req.get('request_date')}\n"
628
+ )
629
+ try:
630
+ bot.send_message(message.chat.id, reply, parse_mode="HTML")
631
+ logging.info("Sent unsubscribe requests list to owner.")
632
+ except Exception as e:
633
+ logging.error(f"Error sending unsubscribe requests list: {e}")
634
+
635
+ @bot.message_handler(commands=['process_unsubscribes'])
636
+ def process_unsubscribes(message):
637
+ """
638
+ Owner‑only command to process unsubscribe requests.
639
+
640
+ <b>Usage Examples:</b>
641
+ β€’ <code>/process_unsubscribes approve all</code>
642
+ β€’ <code>/process_unsubscribes deny all</code>
643
+ β€’ <code>/process_unsubscribes approve 1 3 5</code>
644
+ β€’ <code>/process_unsubscribes deny 2 4</code>
645
+
646
+ β€’ If <b>approve</b> is used, the subscriber record is removed (they will no longer receive alerts).
647
+ β€’ If <b>deny</b> is used, the unsubscribe request is canceled (the subscription remains active).
648
+
649
+ If insufficient parameters are provided, a detailed guide with current pending requests is shown.
650
+ """
651
+ ensure_subscriber(message)
652
+ if message.from_user.id != OWNER_ID:
653
+ try:
654
+ bot.send_message(message.chat.id, "🚫 <b>You are not authorized to process unsubscribe requests.</b>", parse_mode="HTML")
655
+ except Exception as e:
656
+ logging.error(f"Error sending unauthorized process_unsubscribes message: {e}")
657
+ return
658
+
659
+ args = message.text.split()
660
+ reqs = load_unsubscribe_requests()
661
+ if len(args) < 3:
662
+ guide_text = (
663
+ "<b>Usage of /process_unsubscribes:</b>\n\n"
664
+ "<b>To <u>approve</u> unsubscribe requests (remove subscribers):</b>\n"
665
+ " β€’ <code>/process_unsubscribes approve all</code> - Approve all requests\n"
666
+ " β€’ <code>/process_unsubscribes approve 1 3 5</code> - Approve specific requests by their serial numbers\n\n"
667
+ "<b>To <u>deny</u> unsubscribe requests (cancel requests, keep subscription active):</b>\n"
668
+ " β€’ <code>/process_unsubscribes deny all</code> - Deny all requests\n"
669
+ " β€’ <code>/process_unsubscribes deny 2 4</code> - Deny specific requests by their serial numbers\n\n"
670
+ "<b>Current Unsubscribe Requests:</b>\n"
671
+ )
672
+ if reqs:
673
+ for idx, req in enumerate(reqs, start=1):
674
+ guide_text += (f"{idx}. <b>User ID:</b> {req.get('user_id')}, "
675
+ f"<b>Name:</b> {req.get('first_name')} {req.get('last_name')}, "
676
+ f"<b>Username:</b> @{req.get('username')}, "
677
+ f"<b>Requested On:</b> {req.get('request_date')}\n")
678
+ else:
679
+ guide_text += "ℹ️ No unsubscribe requests pending."
680
+ try:
681
+ bot.send_message(message.chat.id, guide_text, parse_mode="HTML")
682
+ except Exception as e:
683
+ logging.error(f"Error sending process_unsubscribes guide: {e}")
684
+ return
685
+
686
+ action = args[1].lower()
687
+ if action not in ("approve", "deny"):
688
+ try:
689
+ bot.send_message(message.chat.id, "ℹ️ <b>Action must be either 'approve' or 'deny'.</b>", parse_mode="HTML")
690
+ except Exception as e:
691
+ logging.error(f"Error sending action message: {e}")
692
+ return
693
+
694
+ targets = args[2:]
695
+ if len(targets) == 1 and targets[0].lower() == "all":
696
+ indices = list(range(len(reqs)))
697
+ else:
698
+ try:
699
+ indices = [int(x) - 1 for x in targets if x.isdigit()]
700
+ except Exception as e:
701
+ try:
702
+ bot.send_message(message.chat.id, "ℹ️ <b>Invalid input. Provide serial numbers or 'all'.</b>", parse_mode="HTML")
703
+ except Exception as ex:
704
+ logging.error(f"Error sending invalid input message: {ex}")
705
+ return
706
+
707
+ indices = sorted(set(indices))
708
+ subscribers = load_subscribers()
709
+ processed_details = []
710
+ if action == "approve":
711
+ for i in indices:
712
+ if i < 0 or i >= len(reqs):
713
+ continue
714
+ req = reqs[i]
715
+ chat_id = req.get("chat_id")
716
+ sub_removed = None
717
+ for sub in subscribers:
718
+ if sub.get("chat_id") == chat_id:
719
+ sub_removed = sub
720
+ break
721
+ if sub_removed:
722
+ subscribers.remove(sub_removed)
723
+ processed_details.append(f"βœ… Approved unsubscribe for <b>{sub_removed.get('first_name')} {sub_removed.get('last_name')}</b> (@{sub_removed.get('username')}) (User ID: {sub_removed.get('user_id')})")
724
+ new_reqs = [req for j, req in enumerate(reqs) if j not in indices]
725
+ elif action == "deny":
726
+ for i in indices:
727
+ if i < 0 or i >= len(reqs):
728
+ continue
729
+ req = reqs[i]
730
+ chat_id = req.get("chat_id")
731
+ for sub in subscribers:
732
+ if sub.get("chat_id") == chat_id:
733
+ sub["unsubscribe_requested"] = False
734
+ processed_details.append(f"❌ Denied unsubscribe for <b>{sub.get('first_name')} {sub.get('last_name')}</b> (@{sub.get('username')}) (User ID: {sub.get('user_id')})")
735
+ break
736
+ new_reqs = [req for j, req in enumerate(reqs) if j not in indices]
737
+ else:
738
+ new_reqs = reqs
739
+
740
+ save_subscribers(subscribers)
741
+ save_unsubscribe_requests(new_reqs)
742
+ if processed_details:
743
+ reply = "<b>Processed Unsubscribe Requests:</b>\n" + "\n".join(processed_details)
744
+ else:
745
+ reply = "ℹ️ <b>No valid unsubscribe requests processed.</b>"
746
+ try:
747
+ bot.send_message(message.chat.id, reply, parse_mode="HTML")
748
+ logging.info(f"Processed unsubscribe requests: {reply}")
749
+ except Exception as e:
750
+ logging.error(f"Error sending process_unsubscribes confirmation: {e}")
751
+
752
+ @bot.message_handler(commands=['clear_chat_logs'])
753
+ def clear_chat_logs(message):
754
+ """
755
+ Owner‑only command to clear chat logs.
756
+
757
+ <b>Usage:</b>
758
+ β€’ <code>/clear_chat_logs all</code> - Clear all chat logs.
759
+ β€’ <code>/clear_chat_logs &lt;chat_id&gt;</code> - Clear the chat log for a specific chat.
760
+
761
+ If the required parameter is missing, a detailed usage guide is shown.
762
+ """
763
+ ensure_subscriber(message)
764
+ if message.from_user.id != OWNER_ID:
765
+ try:
766
+ bot.send_message(message.chat.id, "🚫 <b>You are not authorized to clear chat logs.</b>", parse_mode="HTML")
767
+ except Exception as e:
768
+ logging.error(f"Error sending unauthorized clear_chat_logs message: {e}")
769
+ return
770
+
771
+ args = message.text.split()
772
+ if len(args) < 2:
773
+ usage = (
774
+ "<b>Usage of /clear_chat_logs:</b>\n\n"
775
+ "β€’ <code>/clear_chat_logs all</code> - Clear all chat logs.\n"
776
+ "β€’ <code>/clear_chat_logs &lt;chat_id&gt;</code> - Clear the chat log for a specific chat.\n\n"
777
+ "Example:\n"
778
+ "β€’ <code>/clear_chat_logs all</code>\n"
779
+ "β€’ <code>/clear_chat_logs 123456789</code>"
780
+ )
781
+ try:
782
+ bot.send_message(message.chat.id, usage, parse_mode="HTML")
783
+ except Exception as e:
784
+ logging.error(f"Error sending clear_chat_logs usage guide: {e}")
785
+ return
786
+
787
+ target = args[1].lower()
788
+ if target == "all":
789
+ cleared = 0
790
+ for file in filesystem.listdir(CHAT_LOGS_DIR):
791
+ file_path = path.join(CHAT_LOGS_DIR, file)
792
+ try:
793
+ filesystem.remove(file_path)
794
+ cleared += 1
795
+ except Exception as e:
796
+ logging.error(f"Error removing file {file_path}: {e}")
797
+ reply = f"βœ… Cleared <b>{cleared}</b> chat log file(s)."
798
+ else:
799
+ file_path = path.join(CHAT_LOGS_DIR, f"{target}.log")
800
+ if filesystem.exists(file_path):
801
+ try:
802
+ filesystem.remove(file_path)
803
+ reply = f"βœ… Cleared chat log for chat <b>{target}</b>."
804
+ except Exception as e:
805
+ reply = f"⚠️ Error clearing log for chat <b>{target}</b>: {e}"
806
+ logging.error(reply)
807
+ else:
808
+ reply = f"ℹ️ No log file found for chat <b>{target}</b>."
809
+ try:
810
+ bot.send_message(message.chat.id, reply, parse_mode="HTML")
811
+ logging.info(f"clear_chat_logs: {reply}")
812
+ except Exception as e:
813
+ logging.error(f"Error sending clear_chat_logs confirmation: {e}")
814
+
815
+ @bot.message_handler(commands=['ping'])
816
+ def ping(message):
817
+ """Simple command to check if the bot is responsive."""
818
+ ensure_subscriber(message)
819
+ try:
820
+ bot.send_message(message.chat.id, "πŸ“ <b>pong</b>", parse_mode="HTML")
821
+ logging.info(f"Ping response sent to chat {message.chat.id}.")
822
+ except Exception as e:
823
+ logging.error(f"Error sending ping response: {e}")
824
+
825
+ # ------------------------------------------------------------------------------
826
+ # Default Handler: Log Incoming Messages and Ensure Subscriber Record
827
+ # ------------------------------------------------------------------------------
828
+
829
+ @bot.message_handler(func=lambda message: True)
830
+ def default_message_handler(message):
831
+ """
832
+ For every incoming message:
833
+ 1. Auto-subscribe the user (if not already in the list).
834
+ 2. Log the message in a per‑chat log file.
835
+ """
836
+ try:
837
+ ensure_subscriber(message)
838
+ except Exception as e:
839
+ logging.error(f"Error ensuring subscriber for chat {message.chat.id}: {e}")
840
+ try:
841
+ log_chat_message(message)
842
+ except Exception as e:
843
+ logging.error(f"Error logging message from chat {message.chat.id}: {e}")
844
+ logging.debug(f"Received message in chat {message.chat.id}: from: {message.from_user.username if message.from_user else 'N/A'} | text: {message.text}")
845
+
846
+ # ------------------------------------------------------------------------------
847
+ # Asynchronous Owner Alert Input (Console)
848
+ # ------------------------------------------------------------------------------
849
+
850
+ # def alert_input_listener():
851
+ # """
852
+ # Continuously prompt the owner (via the console) for an alert message.
853
+ # Upon input (unless 'exit' is typed), broadcast the alert to all subscribers.
854
+ # """
855
+ # while True:
856
+ # try:
857
+ # alert_message = input("Enter alert message to broadcast (or type 'exit' to stop): ").strip()
858
+ # if alert_message.lower() == 'exit':
859
+ # logging.info("Exiting console alert input listener.")
860
+ # break
861
+ # if not alert_message:
862
+ # continue # Skip empty input.
863
+ # subscribers = load_subscribers()
864
+ # if not subscribers:
865
+ # logging.warning("No subscribers found. Alert not sent.")
866
+ # continue
867
+ # for sub in subscribers:
868
+ # chat_id = sub.get("chat_id")
869
+ # try:
870
+ # response = bot.send_message(chat_id, alert_message, parse_mode="HTML")
871
+ # logging.info(f"Console alert sent to chat {chat_id} (message_id={response.message_id}).")
872
+ # except Exception as e:
873
+ # logging.error(f"Error sending console alert to chat {chat_id}: {e}")
874
+ # except Exception as e:
875
+ # logging.error(f"Error in alert input listener: {e}")
876
+
877
+ # Start the asynchronous alert input listener in a daemon thread.
878
+ # alert_input_thread = threading.Thread(target=alert_input_listener, daemon=True)
879
+ # alert_input_thread.start()
880
+ # logging.info("Started asynchronous alert input listener thread.")
881
+
882
+ # ------------------------------------------------------------------------------
883
+ # Start Bot Polling
884
+ # ------------------------------------------------------------------------------
885
+
886
+ # try:
887
+ # logging.info("Starting bot polling...")
888
+ # bot.polling(none_stop=True)
889
+ # except Exception as e:
890
+ # logging.error(f"Bot polling error: {e}")
891
+
892
+ # ─────────────────────────────────────────────────── FastAPI ───
893
+ app = FastAPI()
894
+
895
+ @app.get("/")
896
+ def root(): # β‘‘ health‑check hits this β†’ must return 200 quickly
897
+ return {"status": "ok"}
898
+
899
+ @app.on_event("startup")
900
+ def startup():
901
+ # Launch the bot *after* Uvicorn has started
902
+ threading.Thread(target=bot.infinity_polling, daemon=True).start()