Hermes Agent commited on
Commit
904609e
·
1 Parent(s): 2438b28

feat: implement ₹10/month UPI subscription paywall with dual-action admin approval

Browse files
Thunder/bot/plugins/stream.py CHANGED
@@ -1,11 +1,11 @@
1
- # Thunder/bot/plugins/stream.py
2
-
3
- import asyncio
4
- import secrets
5
- from typing import Any, Dict, Optional
6
-
7
- from pyrogram import Client, enums, filters
8
- from pyrogram.errors import FloodWait, MessageNotModified, MessageDeleteForbidden, MessageIdInvalid
9
  from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
10
  Message)
11
 
@@ -16,62 +16,91 @@ from Thunder.utils.canonical_files import get_or_create_canonical_file
16
  from Thunder.utils.database import db
17
  from Thunder.utils.decorators import (check_banned, get_shortener_status,
18
  require_token)
19
- from Thunder.utils.force_channel import force_channel_check
20
- from Thunder.utils.logger import logger
21
- from Thunder.utils.messages import (
22
- MSG_BATCH_LINKS_READY, MSG_BUTTON_DOWNLOAD, MSG_BUTTON_START_CHAT,
23
- MSG_BUTTON_STREAM_NOW, MSG_CRITICAL_ERROR, MSG_DM_BATCH_PREFIX,
24
- MSG_DM_SINGLE_PREFIX, MSG_ERROR_DM_FAILED, MSG_ERROR_INVALID_NUMBER,
25
- MSG_ERROR_NO_FILE, MSG_ERROR_NOT_ADMIN, MSG_ERROR_NUMBER_RANGE,
26
- MSG_ERROR_PROCESSING_MEDIA, MSG_ERROR_REPLY_FILE, MSG_ERROR_START_BOT,
27
- MSG_LINKS, MSG_NEW_FILE_REQUEST, MSG_PROCESSING_BATCH,
28
- MSG_PROCESSING_FILE, MSG_PROCESSING_REQUEST, MSG_PROCESSING_RESULT,
29
- MSG_PROCESSING_STATUS
30
- )
31
- from Thunder.utils.rate_limiter import handle_rate_limited_request
32
- from Thunder.vars import Var
33
-
34
- BATCH_SIZE = 10
35
- LINK_CHUNK_SIZE = 20
36
- BATCH_UPDATE_INTERVAL = 5
37
- MESSAGE_DELAY = 0.5
38
-
39
-
40
- async def fwd_media(m_msg: Message) -> Optional[Message]:
41
- try:
42
- try:
43
- return await m_msg.copy(chat_id=Var.BIN_CHANNEL)
44
- except FloodWait as e:
45
- await asyncio.sleep(e.value)
46
- return await m_msg.copy(chat_id=Var.BIN_CHANNEL)
47
- except Exception as e:
48
- if "MEDIA_CAPTION_TOO_LONG" in str(e):
49
- logger.debug(f"MEDIA_CAPTION_TOO_LONG error, retrying without caption: {e}")
50
- try:
51
- return await m_msg.copy(chat_id=Var.BIN_CHANNEL, caption=None)
52
- except FloodWait as e:
53
- await asyncio.sleep(e.value)
54
- return await m_msg.copy(chat_id=Var.BIN_CHANNEL, caption=None)
55
- logger.error(f"Error fwd_media copy: {e}", exc_info=True)
56
- return None
57
-
58
-
59
- def get_link_buttons(links):
60
- return InlineKeyboardMarkup([[
61
- InlineKeyboardButton(MSG_BUTTON_STREAM_NOW, url=links['stream_link']),
62
- InlineKeyboardButton(MSG_BUTTON_DOWNLOAD, url=links['online_link'])
63
- ]])
64
-
65
- async def validate_request_common(client: Client, message: Message) -> Optional[bool]:
66
- if not await check_banned(client, message):
67
- return None
68
- if not await require_token(client, message):
69
- return None
70
- if not await force_channel_check(client, message):
71
- return None
72
- return await get_shortener_status(client, message)
73
-
74
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  async def send_channel_links(
76
  links: Dict[str, Any],
77
  source_info: str,
@@ -121,224 +150,230 @@ async def send_channel_links(
121
  disable_web_page_preview=True,
122
  reply_to_message_id=reply_to_message_id
123
  )
124
-
125
-
126
- async def safe_edit_message(message: Message, text: str, **kwargs):
127
- try:
128
- try:
129
- return await message.edit_text(text, **kwargs)
130
- except FloodWait as e:
131
- await asyncio.sleep(e.value)
132
- return await message.edit_text(text, **kwargs)
133
- except MessageNotModified:
134
- pass
135
- except MessageDeleteForbidden:
136
- logger.debug(f"Failed to edit message {message.id} due to permissions.")
137
- except Exception as e:
138
- logger.error(f"Error editing message {message.id}: {e}", exc_info=True)
139
-
140
-
141
- async def safe_delete_message(message: Message):
142
- try:
143
- try:
144
- await message.delete()
145
- except FloodWait as e:
146
- await asyncio.sleep(e.value)
147
- await message.delete()
148
- except MessageDeleteForbidden:
149
- logger.debug(f"Failed to delete message {message.id} due to permissions.")
150
- except Exception as e:
151
- logger.error(f"Error deleting message {message.id}: {e}", exc_info=True)
152
-
153
-
154
- async def send_dm_links(bot: Client, user_id: int, links: Dict[str, Any], chat_title: str):
155
- try:
156
- dm_text = MSG_DM_SINGLE_PREFIX.format(chat_title=chat_title) + "\n" + \
157
- MSG_LINKS.format(
158
- file_name=links['media_name'],
159
- file_size=links['media_size'],
160
- download_link=links['online_link'],
161
- stream_link=links['stream_link']
162
- )
163
- try:
164
- await bot.send_message(
165
- chat_id=user_id,
166
- text=dm_text,
167
- disable_web_page_preview=True,
168
- parse_mode=enums.ParseMode.MARKDOWN,
169
- reply_markup=get_link_buttons(links)
170
- )
171
- except FloodWait as e:
172
- await asyncio.sleep(e.value)
173
- await bot.send_message(
174
- chat_id=user_id,
175
- text=dm_text,
176
- disable_web_page_preview=True,
177
- parse_mode=enums.ParseMode.MARKDOWN,
178
- reply_markup=get_link_buttons(links)
179
- )
180
- except Exception as e:
181
- logger.error(f"Error sending DM to user {user_id}: {e}", exc_info=True)
182
-
183
-
184
- async def send_link(msg: Message, links: Dict[str, Any]):
185
- try:
186
- await msg.reply_text(
187
- MSG_LINKS.format(
188
- file_name=links['media_name'],
189
- file_size=links['media_size'],
190
- download_link=links['online_link'],
191
- stream_link=links['stream_link']
192
- ),
193
- quote=True,
194
- parse_mode=enums.ParseMode.MARKDOWN,
195
- disable_web_page_preview=True,
196
- reply_markup=get_link_buttons(links)
197
- )
198
- except FloodWait as e:
199
- await asyncio.sleep(e.value)
200
- await msg.reply_text(
201
- MSG_LINKS.format(
202
- file_name=links['media_name'],
203
- file_size=links['media_size'],
204
- download_link=links['online_link'],
205
- stream_link=links['stream_link']
206
- ),
207
- quote=True,
208
- parse_mode=enums.ParseMode.MARKDOWN,
209
- disable_web_page_preview=True,
210
- reply_markup=get_link_buttons(links)
211
- )
212
-
213
-
214
- @StreamBot.on_message(filters.command("link") & ~filters.private)
215
- async def link_handler(bot: Client, msg: Message, **kwargs):
216
- async def _actual_link_handler(client: Client, message: Message, **handler_kwargs):
217
- shortener_val = await validate_request_common(client, message)
218
- if shortener_val is None:
219
- return
220
- if message.from_user and not await db.is_user_exist(message.from_user.id):
221
- invite_link = f"https://t.me/{client.me.username}?start=start"
222
- try:
223
- await message.reply_text(
224
- MSG_ERROR_START_BOT.format(invite_link=invite_link),
225
- disable_web_page_preview=True,
226
- parse_mode=enums.ParseMode.MARKDOWN,
227
- reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_START_CHAT, url=invite_link)]]),
228
- quote=True
229
- )
230
- except FloodWait as e:
231
- await asyncio.sleep(e.value)
232
- await message.reply_text(
233
- MSG_ERROR_START_BOT.format(invite_link=invite_link),
234
- disable_web_page_preview=True,
235
- parse_mode=enums.ParseMode.MARKDOWN,
236
- reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_START_CHAT, url=invite_link)]]),
237
- quote=True
238
- )
239
- return
240
-
241
- if (message.chat.type in [enums.ChatType.GROUP, enums.ChatType.SUPERGROUP]
242
- and not await is_admin(client, message.chat.id)):
243
- await reply_user_err(message, MSG_ERROR_NOT_ADMIN)
244
- return
245
-
246
- if not message.reply_to_message or not message.reply_to_message.media:
247
- await reply_user_err(
248
- message,
249
- MSG_ERROR_REPLY_FILE if not message.reply_to_message else MSG_ERROR_NO_FILE)
250
- return
251
-
252
- notification_msg = handler_kwargs.get('notification_msg')
253
-
254
- parts = message.text.split()
255
- num_files = 1
256
- if len(parts) > 1:
257
- try:
258
- num_files = int(parts[1])
259
- if not 1 <= num_files <= Var.MAX_BATCH_FILES:
260
- await reply_user_err(
261
- message,
262
- MSG_ERROR_NUMBER_RANGE.format(max_files=Var.MAX_BATCH_FILES))
263
- return
264
- except ValueError:
265
- await reply_user_err(message, MSG_ERROR_INVALID_NUMBER)
266
- return
267
-
268
- try:
269
- status_msg = await message.reply_text(MSG_PROCESSING_REQUEST, quote=True)
270
- except FloodWait as e:
271
- await asyncio.sleep(e.value)
272
- status_msg = await message.reply_text(MSG_PROCESSING_REQUEST, quote=True)
273
- shortener_val = handler_kwargs.get('shortener', shortener_val)
274
- if num_files == 1:
275
- await process_single(client, message, message.reply_to_message, status_msg, shortener_val, notification_msg=notification_msg)
276
- else:
277
- await process_batch(client, message, message.reply_to_message.id, num_files, status_msg, shortener_val, notification_msg=notification_msg)
278
-
279
- await handle_rate_limited_request(bot, msg, _actual_link_handler, **kwargs)
280
-
281
-
282
- @StreamBot.on_message(
283
- filters.private &
284
- filters.incoming &
285
- (filters.document | filters.video | filters.photo | filters.audio |
286
- filters.voice | filters.animation | filters.video_note),
287
- group=4
288
- )
289
- async def private_receive_handler(bot: Client, msg: Message, **kwargs):
290
- async def _actual_private_receive_handler(client: Client, message: Message, **handler_kwargs):
291
- shortener_val = await validate_request_common(client, message)
292
- if shortener_val is None:
293
- return
294
- if not message.from_user:
295
- return
296
-
297
- notification_msg = handler_kwargs.get('notification_msg')
298
-
299
- await log_newusr(client, message.from_user.id, message.from_user.first_name or "")
300
- try:
301
- status_msg = await message.reply_text(MSG_PROCESSING_FILE, quote=True)
302
- except FloodWait as e:
303
- await asyncio.sleep(e.value)
304
- status_msg = await message.reply_text(MSG_PROCESSING_FILE, quote=True)
305
- await process_single(client, message, message, status_msg, shortener_val, notification_msg=notification_msg)
306
-
307
- await handle_rate_limited_request(bot, msg, _actual_private_receive_handler, **kwargs)
308
-
309
-
310
- @StreamBot.on_message(
311
- filters.channel &
312
- filters.incoming &
313
- (filters.document | filters.video | filters.audio) &
314
- ~filters.chat(Var.BIN_CHANNEL),
315
- group=-1
316
- )
317
- async def channel_receive_handler(bot: Client, msg: Message):
318
- async def _actual_channel_receive_handler(client: Client, message: Message, **handler_kwargs):
319
- if not Var.CHANNEL:
320
- return
321
- notification_msg = handler_kwargs.get('notification_msg')
322
-
323
- is_banned_statically = hasattr(Var, 'BANNED_CHANNELS') and message.chat.id in Var.BANNED_CHANNELS
324
- is_banned_dynamically = await db.is_channel_banned(message.chat.id) is not None
325
-
326
- if is_banned_statically or is_banned_dynamically:
327
- try:
328
- try:
329
- await client.leave_chat(message.chat.id)
330
- except FloodWait as e:
331
- await asyncio.sleep(e.value)
332
- await client.leave_chat(message.chat.id)
333
- except Exception as e:
334
- logger.error(f"Error leaving banned channel {message.chat.id}: {e}")
335
- return
336
- if not await is_admin(client, message.chat.id):
337
- logger.debug(
338
- f"Bot is not admin in channel {message.chat.id} "
339
- f"({message.chat.title or 'Unknown'}). Ignoring message.")
340
- return
341
-
 
 
 
 
 
 
342
  try:
343
  shortener_val = await get_shortener_status(client, message)
344
  canonical_record, stored_msg, reused_existing = await get_or_create_canonical_file(message, fwd_media)
@@ -369,27 +404,27 @@ async def channel_receive_handler(bot: Client, msg: Message):
369
 
370
  if notification_msg:
371
  try:
372
- try:
373
- await notification_msg.edit_text(
374
- MSG_NEW_FILE_REQUEST.format(
375
- source_info=source_info,
376
- id_=message.chat.id,
377
- online_link=links['online_link'],
378
- stream_link=links['stream_link']
379
- ),
380
- disable_web_page_preview=True
381
- )
382
- except FloodWait as e:
383
- await asyncio.sleep(e.value)
384
- await notification_msg.edit_text(
385
- MSG_NEW_FILE_REQUEST.format(
386
- source_info=source_info,
387
- id_=message.chat.id,
388
- online_link=links['online_link'],
389
- stream_link=links['stream_link']
390
- ),
391
- disable_web_page_preview=True
392
- )
393
  except Exception as e:
394
  logger.error(f"Error editing notification message with links: {e}", exc_info=True)
395
  await send_channel_links(
@@ -407,43 +442,43 @@ async def channel_receive_handler(bot: Client, msg: Message):
407
  target_msg=stored_msg,
408
  reply_to_message_id=reply_to_message_id
409
  )
410
-
411
- try:
412
- try:
413
- await message.edit_reply_markup(reply_markup=get_link_buttons(links))
414
- except FloodWait as e:
415
- await asyncio.sleep(e.value)
416
- await message.edit_reply_markup(reply_markup=get_link_buttons(links))
417
- except (MessageNotModified, MessageDeleteForbidden, MessageIdInvalid):
418
- logger.debug(f"Failed to edit reply markup for message {message.id} due to not modified, permissions or invalid ID. Sending new link instead.")
419
- await send_link(message, links)
420
- except Exception as e:
421
- logger.error(f"Error editing reply markup for message {message.id}: {e}", exc_info=True)
422
- await send_link(message, links)
423
- except Exception as e:
424
- logger.error(f"Error in _actual_channel_receive_handler for message {message.id}: {e}", exc_info=True)
425
-
426
- rl_user_id = None
427
- if msg.sender_chat and msg.sender_chat.id:
428
- rl_user_id = msg.sender_chat.id
429
- elif msg.from_user:
430
- rl_user_id = msg.from_user.id
431
-
432
- if rl_user_id is None:
433
- logger.debug(f"No identifiable user/channel for rate limiting for message {msg.id}. Skipping rate limit check and processing directly.")
434
- await _actual_channel_receive_handler(bot, msg)
435
- return
436
-
437
- await handle_rate_limited_request(bot, msg, _actual_channel_receive_handler, rl_user_id=rl_user_id)
438
-
439
-
440
- async def process_single(
441
- bot: Client,
442
- msg: Message,
443
- file_msg: Message,
444
- status_msg: Message,
445
- shortener_val: bool,
446
- original_request_msg: Optional[Message] = None,
447
  notification_msg: Optional[Message] = None
448
  ):
449
  try:
@@ -467,33 +502,33 @@ async def process_single(
467
  return None
468
  links = await gen_links(stored_msg, shortener=shortener_val)
469
  canonical_reply_id = stored_msg.id
470
- if notification_msg:
471
- result = await safe_edit_message(
472
- notification_msg,
473
- MSG_LINKS.format(
474
- file_name=links['media_name'],
475
- file_size=links['media_size'],
476
- download_link=links['online_link'],
477
- stream_link=links['stream_link']
478
- ),
479
- parse_mode=enums.ParseMode.MARKDOWN,
480
- disable_web_page_preview=True,
481
- reply_markup=get_link_buttons(links)
482
- )
483
- if not result:
484
- await send_link(msg, links)
485
  elif not original_request_msg:
486
- await send_link(msg, links)
487
- if msg.chat.type != enums.ChatType.PRIVATE and msg.from_user and not original_request_msg:
488
- await send_dm_links(bot, msg.from_user.id, links, msg.chat.title or "the chat")
489
- source_msg = original_request_msg if original_request_msg else msg
490
- source_info = ""
491
- source_id = 0
492
- if source_msg.from_user:
493
- source_info = source_msg.from_user.full_name
494
- if not source_info:
495
- source_info = f"@{source_msg.from_user.username}" if source_msg.from_user.username else "Unknown User"
496
- source_id = source_msg.from_user.id
497
  elif source_msg.chat.type == enums.ChatType.CHANNEL:
498
  source_info = source_msg.chat.title or "Unknown Channel"
499
  source_id = source_msg.chat.id
@@ -517,152 +552,152 @@ async def process_single(
517
  )
518
  if status_msg:
519
  await safe_delete_message(status_msg)
520
- return links
521
- except Exception as e:
522
- logger.error(f"Error processing single file for message {file_msg.id}: {e}", exc_info=True)
523
- if status_msg:
524
- await safe_edit_message(status_msg, MSG_ERROR_PROCESSING_MEDIA)
525
-
526
- await notify_own(bot, MSG_CRITICAL_ERROR.format(
527
- error=str(e),
528
- error_id=secrets.token_hex(6)
529
- ))
530
- return None
531
-
532
-
533
- async def process_batch(
534
- bot: Client,
535
- msg: Message,
536
- start_id: int,
537
- count: int,
538
- status_msg: Message,
539
- shortener_val: bool,
540
- notification_msg: Optional[Message] = None
541
- ):
542
- processed = 0
543
- failed = 0
544
- links_list = []
545
- for batch_start in range(0, count, BATCH_SIZE):
546
- batch_size = min(BATCH_SIZE, count - batch_start)
547
- batch_ids = list(range(start_id + batch_start, start_id + batch_start + batch_size))
548
- try:
549
- try:
550
- await status_msg.edit_text(
551
- MSG_PROCESSING_BATCH.format(
552
- batch_number=(batch_start // BATCH_SIZE) + 1,
553
- total_batches=(count + BATCH_SIZE - 1) // BATCH_SIZE,
554
- file_count=batch_size
555
- )
556
- )
557
- except FloodWait as e:
558
- await asyncio.sleep(e.value)
559
- await status_msg.edit_text(
560
- MSG_PROCESSING_BATCH.format(
561
- batch_number=(batch_start // BATCH_SIZE) + 1,
562
- total_batches=(count + BATCH_SIZE - 1) // BATCH_SIZE,
563
- file_count=batch_size
564
- )
565
- )
566
- except MessageNotModified:
567
- pass
568
- try:
569
- try:
570
- messages = await bot.get_messages(msg.chat.id, batch_ids)
571
- except FloodWait as e:
572
- await asyncio.sleep(e.value)
573
- messages = await bot.get_messages(msg.chat.id, batch_ids)
574
- if messages is None:
575
- messages = []
576
- except Exception as e:
577
- logger.error(f"Error getting messages in batch: {e}", exc_info=True)
578
- messages = []
579
- for m in messages:
580
- if m and m.media:
581
- links = await process_single(bot, msg, m, None, shortener_val, original_request_msg=msg)
582
- if links:
583
- links_list.append(links['online_link'])
584
- processed += 1
585
- else:
586
- failed += 1
587
- else:
588
- failed += 1
589
- if (processed + failed) % BATCH_UPDATE_INTERVAL == 0 or (processed + failed) == count:
590
- try:
591
- try:
592
- await status_msg.edit_text(
593
- MSG_PROCESSING_STATUS.format(
594
- processed=processed,
595
- total=count,
596
- failed=failed
597
- )
598
- )
599
- except FloodWait as e:
600
- await asyncio.sleep(e.value)
601
- await status_msg.edit_text(
602
- MSG_PROCESSING_STATUS.format(
603
- processed=processed,
604
- total=count,
605
- failed=failed
606
- )
607
- )
608
- except MessageNotModified:
609
- pass
610
- for i in range(0, len(links_list), LINK_CHUNK_SIZE):
611
- chunk = links_list[i:i+LINK_CHUNK_SIZE]
612
- chunk_text = MSG_BATCH_LINKS_READY.format(count=len(chunk)) + f"\n\n<code>{chr(10).join(chunk)}</code>"
613
- try:
614
- await msg.reply_text(
615
- chunk_text,
616
- quote=True,
617
- disable_web_page_preview=True,
618
- parse_mode=enums.ParseMode.HTML
619
- )
620
- except FloodWait as e:
621
- await asyncio.sleep(e.value)
622
- await msg.reply_text(
623
- chunk_text,
624
- quote=True,
625
- disable_web_page_preview=True,
626
- parse_mode=enums.ParseMode.HTML
627
- )
628
- if msg.chat.type != enums.ChatType.PRIVATE and msg.from_user:
629
- try:
630
- try:
631
- await bot.send_message(
632
- chat_id=msg.from_user.id,
633
- text=MSG_DM_BATCH_PREFIX.format(chat_title=msg.chat.title or "the chat") + "\n" + chunk_text,
634
- disable_web_page_preview=True,
635
- parse_mode=enums.ParseMode.HTML
636
- )
637
- except FloodWait as e:
638
- await asyncio.sleep(e.value)
639
- await bot.send_message(
640
- chat_id=msg.from_user.id,
641
- text=MSG_DM_BATCH_PREFIX.format(chat_title=msg.chat.title or "the chat") + "\n" + chunk_text,
642
- disable_web_page_preview=True,
643
- parse_mode=enums.ParseMode.HTML
644
- )
645
- except Exception as e:
646
- logger.error(f"Error sending DM in batch: {e}", exc_info=True)
647
- await reply_user_err(msg, MSG_ERROR_DM_FAILED)
648
- if i + LINK_CHUNK_SIZE < len(links_list):
649
- await asyncio.sleep(MESSAGE_DELAY)
650
- try:
651
- await status_msg.edit_text(
652
- MSG_PROCESSING_RESULT.format(
653
- processed=processed,
654
- total=count,
655
- failed=failed
656
- )
657
- )
658
- except FloodWait as e:
659
- await asyncio.sleep(e.value)
660
- await status_msg.edit_text(
661
- MSG_PROCESSING_RESULT.format(
662
- processed=processed,
663
- total=count,
664
- failed=failed
665
- )
666
- )
667
- if notification_msg:
668
- await safe_delete_message(notification_msg)
 
1
+ # Thunder/bot/plugins/stream.py
2
+
3
+ import asyncio
4
+ import secrets
5
+ from typing import Any, Dict, Optional
6
+
7
+ from pyrogram import Client, enums, filters
8
+ from pyrogram.errors import FloodWait, MessageNotModified, MessageDeleteForbidden, MessageIdInvalid
9
  from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
10
  Message)
11
 
 
16
  from Thunder.utils.database import db
17
  from Thunder.utils.decorators import (check_banned, get_shortener_status,
18
  require_token)
19
+ from Thunder.utils.force_channel import force_channel_check
20
+ from Thunder.utils.logger import logger
21
+ from Thunder.utils.messages import (
22
+ MSG_BATCH_LINKS_READY, MSG_PAYWALL_GATE, MSG_BUTTON_DOWNLOAD, MSG_BUTTON_START_CHAT,
23
+ MSG_BUTTON_STREAM_NOW, MSG_CRITICAL_ERROR, MSG_DM_BATCH_PREFIX,
24
+ MSG_DM_SINGLE_PREFIX, MSG_ERROR_DM_FAILED, MSG_ERROR_INVALID_NUMBER,
25
+ MSG_ERROR_NO_FILE, MSG_ERROR_NOT_ADMIN, MSG_ERROR_NUMBER_RANGE,
26
+ MSG_ERROR_PROCESSING_MEDIA, MSG_ERROR_REPLY_FILE, MSG_ERROR_START_BOT,
27
+ MSG_LINKS, MSG_NEW_FILE_REQUEST, MSG_PROCESSING_BATCH,
28
+ MSG_PROCESSING_FILE, MSG_PROCESSING_REQUEST, MSG_PROCESSING_RESULT,
29
+ MSG_PROCESSING_STATUS
30
+ )
31
+ from Thunder.utils.rate_limiter import handle_rate_limited_request
32
+ from Thunder.vars import Var
33
+
34
+ BATCH_SIZE = 10
35
+ LINK_CHUNK_SIZE = 20
36
+ BATCH_UPDATE_INTERVAL = 5
37
+ MESSAGE_DELAY = 0.5
38
+
39
+
40
+ async def fwd_media(m_msg: Message) -> Optional[Message]:
41
+ try:
42
+ try:
43
+ return await m_msg.copy(chat_id=Var.BIN_CHANNEL)
44
+ except FloodWait as e:
45
+ await asyncio.sleep(e.value)
46
+ return await m_msg.copy(chat_id=Var.BIN_CHANNEL)
47
+ except Exception as e:
48
+ if "MEDIA_CAPTION_TOO_LONG" in str(e):
49
+ logger.debug(f"MEDIA_CAPTION_TOO_LONG error, retrying without caption: {e}")
50
+ try:
51
+ return await m_msg.copy(chat_id=Var.BIN_CHANNEL, caption=None)
52
+ except FloodWait as e:
53
+ await asyncio.sleep(e.value)
54
+ return await m_msg.copy(chat_id=Var.BIN_CHANNEL, caption=None)
55
+ logger.error(f"Error fwd_media copy: {e}", exc_info=True)
56
+ return None
57
+
58
+
59
+ def get_link_buttons(links):
60
+ return InlineKeyboardMarkup([[
61
+ InlineKeyboardButton(MSG_BUTTON_STREAM_NOW, url=links['stream_link']),
62
+ InlineKeyboardButton(MSG_BUTTON_DOWNLOAD, url=links['online_link'])
63
+ ]])
64
+
65
+
66
+ async def send_paywall_card(client: Client, message: Message, file_id: Optional[str] = None):
67
+ try:
68
+ user_id = message.from_user.id
69
+ from Thunder.bot.plugins.subscription import get_plan_keyboard
70
+ text = MSG_PAYWALL_GATE.format(
71
+ price=Var.SUB_PRICE,
72
+ upi_id=Var.UPI_ID
73
+ )
74
+ keyboard = get_plan_keyboard(user_id, file_id=file_id)
75
+ await message.reply_text(text, reply_markup=keyboard, quote=True)
76
+ except Exception as e:
77
+ logger.error(f"Error sending paywall card: {e}", exc_info=True)
78
+
79
+ async def validate_request_common(client: Client, message: Message, file_identifier: Optional[str] = None) -> Optional[bool]:
80
+ if not await check_banned(client, message):
81
+ return None
82
+ if not await require_token(client, message):
83
+ return None
84
+ if not await force_channel_check(client, message):
85
+ return None
86
+
87
+ # Subscription / Payment Paywall Gate (Dormant by default)
88
+ if Var.PAYMENT_ENABLED and message.from_user:
89
+ user_id = message.from_user.id
90
+ is_owner = user_id == Var.OWNER_ID
91
+ is_admin_user = hasattr(Var, 'ADMINS') and user_id in Var.ADMINS
92
+ if not (is_owner or is_admin_user):
93
+ is_sub = await db.is_subscribed(user_id)
94
+ has_temp = False
95
+ if not is_sub and file_identifier:
96
+ has_temp = await db.has_temp_pass(user_id, file_identifier)
97
+ if not (is_sub or has_temp):
98
+ await send_paywall_card(client, message, file_id=file_identifier)
99
+ return None
100
+
101
+ return await get_shortener_status(client, message)
102
+
103
+
104
  async def send_channel_links(
105
  links: Dict[str, Any],
106
  source_info: str,
 
150
  disable_web_page_preview=True,
151
  reply_to_message_id=reply_to_message_id
152
  )
153
+
154
+
155
+ async def safe_edit_message(message: Message, text: str, **kwargs):
156
+ try:
157
+ try:
158
+ return await message.edit_text(text, **kwargs)
159
+ except FloodWait as e:
160
+ await asyncio.sleep(e.value)
161
+ return await message.edit_text(text, **kwargs)
162
+ except MessageNotModified:
163
+ pass
164
+ except MessageDeleteForbidden:
165
+ logger.debug(f"Failed to edit message {message.id} due to permissions.")
166
+ except Exception as e:
167
+ logger.error(f"Error editing message {message.id}: {e}", exc_info=True)
168
+
169
+
170
+ async def safe_delete_message(message: Message):
171
+ try:
172
+ try:
173
+ await message.delete()
174
+ except FloodWait as e:
175
+ await asyncio.sleep(e.value)
176
+ await message.delete()
177
+ except MessageDeleteForbidden:
178
+ logger.debug(f"Failed to delete message {message.id} due to permissions.")
179
+ except Exception as e:
180
+ logger.error(f"Error deleting message {message.id}: {e}", exc_info=True)
181
+
182
+
183
+ async def send_dm_links(bot: Client, user_id: int, links: Dict[str, Any], chat_title: str):
184
+ try:
185
+ dm_text = MSG_DM_SINGLE_PREFIX.format(chat_title=chat_title) + "\n" + \
186
+ MSG_LINKS.format(
187
+ file_name=links['media_name'],
188
+ file_size=links['media_size'],
189
+ download_link=links['online_link'],
190
+ stream_link=links['stream_link']
191
+ )
192
+ try:
193
+ await bot.send_message(
194
+ chat_id=user_id,
195
+ text=dm_text,
196
+ disable_web_page_preview=True,
197
+ parse_mode=enums.ParseMode.MARKDOWN,
198
+ reply_markup=get_link_buttons(links)
199
+ )
200
+ except FloodWait as e:
201
+ await asyncio.sleep(e.value)
202
+ await bot.send_message(
203
+ chat_id=user_id,
204
+ text=dm_text,
205
+ disable_web_page_preview=True,
206
+ parse_mode=enums.ParseMode.MARKDOWN,
207
+ reply_markup=get_link_buttons(links)
208
+ )
209
+ except Exception as e:
210
+ logger.error(f"Error sending DM to user {user_id}: {e}", exc_info=True)
211
+
212
+
213
+ async def send_link(msg: Message, links: Dict[str, Any]):
214
+ try:
215
+ await msg.reply_text(
216
+ MSG_LINKS.format(
217
+ file_name=links['media_name'],
218
+ file_size=links['media_size'],
219
+ download_link=links['online_link'],
220
+ stream_link=links['stream_link']
221
+ ),
222
+ quote=True,
223
+ parse_mode=enums.ParseMode.MARKDOWN,
224
+ disable_web_page_preview=True,
225
+ reply_markup=get_link_buttons(links)
226
+ )
227
+ except FloodWait as e:
228
+ await asyncio.sleep(e.value)
229
+ await msg.reply_text(
230
+ MSG_LINKS.format(
231
+ file_name=links['media_name'],
232
+ file_size=links['media_size'],
233
+ download_link=links['online_link'],
234
+ stream_link=links['stream_link']
235
+ ),
236
+ quote=True,
237
+ parse_mode=enums.ParseMode.MARKDOWN,
238
+ disable_web_page_preview=True,
239
+ reply_markup=get_link_buttons(links)
240
+ )
241
+
242
+
243
+ @StreamBot.on_message(filters.command("link") & ~filters.private)
244
+ async def link_handler(bot: Client, msg: Message, **kwargs):
245
+ async def _actual_link_handler(client: Client, message: Message, **handler_kwargs):
246
+ media = (
247
+ message.document or message.video or message.photo or
248
+ message.audio or message.voice or message.animation or message.video_note
249
+ )
250
+ file_unique_id = getattr(media, "file_unique_id", str(message.id)) if media else str(message.id)
251
+
252
+ shortener_val = await validate_request_common(client, message, file_identifier=file_unique_id)
253
+ if shortener_val is None:
254
+ return
255
+ if message.from_user and not await db.is_user_exist(message.from_user.id):
256
+ invite_link = f"https://t.me/{client.me.username}?start=start"
257
+ try:
258
+ await message.reply_text(
259
+ MSG_ERROR_START_BOT.format(invite_link=invite_link),
260
+ disable_web_page_preview=True,
261
+ parse_mode=enums.ParseMode.MARKDOWN,
262
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_START_CHAT, url=invite_link)]]),
263
+ quote=True
264
+ )
265
+ except FloodWait as e:
266
+ await asyncio.sleep(e.value)
267
+ await message.reply_text(
268
+ MSG_ERROR_START_BOT.format(invite_link=invite_link),
269
+ disable_web_page_preview=True,
270
+ parse_mode=enums.ParseMode.MARKDOWN,
271
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_START_CHAT, url=invite_link)]]),
272
+ quote=True
273
+ )
274
+ return
275
+
276
+ if (message.chat.type in [enums.ChatType.GROUP, enums.ChatType.SUPERGROUP]
277
+ and not await is_admin(client, message.chat.id)):
278
+ await reply_user_err(message, MSG_ERROR_NOT_ADMIN)
279
+ return
280
+
281
+ if not message.reply_to_message or not message.reply_to_message.media:
282
+ await reply_user_err(
283
+ message,
284
+ MSG_ERROR_REPLY_FILE if not message.reply_to_message else MSG_ERROR_NO_FILE)
285
+ return
286
+
287
+ notification_msg = handler_kwargs.get('notification_msg')
288
+
289
+ parts = message.text.split()
290
+ num_files = 1
291
+ if len(parts) > 1:
292
+ try:
293
+ num_files = int(parts[1])
294
+ if not 1 <= num_files <= Var.MAX_BATCH_FILES:
295
+ await reply_user_err(
296
+ message,
297
+ MSG_ERROR_NUMBER_RANGE.format(max_files=Var.MAX_BATCH_FILES))
298
+ return
299
+ except ValueError:
300
+ await reply_user_err(message, MSG_ERROR_INVALID_NUMBER)
301
+ return
302
+
303
+ try:
304
+ status_msg = await message.reply_text(MSG_PROCESSING_REQUEST, quote=True)
305
+ except FloodWait as e:
306
+ await asyncio.sleep(e.value)
307
+ status_msg = await message.reply_text(MSG_PROCESSING_REQUEST, quote=True)
308
+ shortener_val = handler_kwargs.get('shortener', shortener_val)
309
+ if num_files == 1:
310
+ await process_single(client, message, message.reply_to_message, status_msg, shortener_val, notification_msg=notification_msg)
311
+ else:
312
+ await process_batch(client, message, message.reply_to_message.id, num_files, status_msg, shortener_val, notification_msg=notification_msg)
313
+
314
+ await handle_rate_limited_request(bot, msg, _actual_link_handler, **kwargs)
315
+
316
+
317
+ @StreamBot.on_message(
318
+ filters.private &
319
+ filters.incoming &
320
+ (filters.document | filters.video | filters.photo | filters.audio |
321
+ filters.voice | filters.animation | filters.video_note),
322
+ group=4
323
+ )
324
+ async def private_receive_handler(bot: Client, msg: Message, **kwargs):
325
+ async def _actual_private_receive_handler(client: Client, message: Message, **handler_kwargs):
326
+ shortener_val = await validate_request_common(client, message)
327
+ if shortener_val is None:
328
+ return
329
+ if not message.from_user:
330
+ return
331
+
332
+ notification_msg = handler_kwargs.get('notification_msg')
333
+
334
+ await log_newusr(client, message.from_user.id, message.from_user.first_name or "")
335
+ try:
336
+ status_msg = await message.reply_text(MSG_PROCESSING_FILE, quote=True)
337
+ except FloodWait as e:
338
+ await asyncio.sleep(e.value)
339
+ status_msg = await message.reply_text(MSG_PROCESSING_FILE, quote=True)
340
+ await process_single(client, message, message, status_msg, shortener_val, notification_msg=notification_msg)
341
+
342
+ await handle_rate_limited_request(bot, msg, _actual_private_receive_handler, **kwargs)
343
+
344
+
345
+ @StreamBot.on_message(
346
+ filters.channel &
347
+ filters.incoming &
348
+ (filters.document | filters.video | filters.audio) &
349
+ ~filters.chat(Var.BIN_CHANNEL),
350
+ group=-1
351
+ )
352
+ async def channel_receive_handler(bot: Client, msg: Message):
353
+ async def _actual_channel_receive_handler(client: Client, message: Message, **handler_kwargs):
354
+ if not Var.CHANNEL:
355
+ return
356
+ notification_msg = handler_kwargs.get('notification_msg')
357
+
358
+ is_banned_statically = hasattr(Var, 'BANNED_CHANNELS') and message.chat.id in Var.BANNED_CHANNELS
359
+ is_banned_dynamically = await db.is_channel_banned(message.chat.id) is not None
360
+
361
+ if is_banned_statically or is_banned_dynamically:
362
+ try:
363
+ try:
364
+ await client.leave_chat(message.chat.id)
365
+ except FloodWait as e:
366
+ await asyncio.sleep(e.value)
367
+ await client.leave_chat(message.chat.id)
368
+ except Exception as e:
369
+ logger.error(f"Error leaving banned channel {message.chat.id}: {e}")
370
+ return
371
+ if not await is_admin(client, message.chat.id):
372
+ logger.debug(
373
+ f"Bot is not admin in channel {message.chat.id} "
374
+ f"({message.chat.title or 'Unknown'}). Ignoring message.")
375
+ return
376
+
377
  try:
378
  shortener_val = await get_shortener_status(client, message)
379
  canonical_record, stored_msg, reused_existing = await get_or_create_canonical_file(message, fwd_media)
 
404
 
405
  if notification_msg:
406
  try:
407
+ try:
408
+ await notification_msg.edit_text(
409
+ MSG_NEW_FILE_REQUEST.format(
410
+ source_info=source_info,
411
+ id_=message.chat.id,
412
+ online_link=links['online_link'],
413
+ stream_link=links['stream_link']
414
+ ),
415
+ disable_web_page_preview=True
416
+ )
417
+ except FloodWait as e:
418
+ await asyncio.sleep(e.value)
419
+ await notification_msg.edit_text(
420
+ MSG_NEW_FILE_REQUEST.format(
421
+ source_info=source_info,
422
+ id_=message.chat.id,
423
+ online_link=links['online_link'],
424
+ stream_link=links['stream_link']
425
+ ),
426
+ disable_web_page_preview=True
427
+ )
428
  except Exception as e:
429
  logger.error(f"Error editing notification message with links: {e}", exc_info=True)
430
  await send_channel_links(
 
442
  target_msg=stored_msg,
443
  reply_to_message_id=reply_to_message_id
444
  )
445
+
446
+ try:
447
+ try:
448
+ await message.edit_reply_markup(reply_markup=get_link_buttons(links))
449
+ except FloodWait as e:
450
+ await asyncio.sleep(e.value)
451
+ await message.edit_reply_markup(reply_markup=get_link_buttons(links))
452
+ except (MessageNotModified, MessageDeleteForbidden, MessageIdInvalid):
453
+ logger.debug(f"Failed to edit reply markup for message {message.id} due to not modified, permissions or invalid ID. Sending new link instead.")
454
+ await send_link(message, links)
455
+ except Exception as e:
456
+ logger.error(f"Error editing reply markup for message {message.id}: {e}", exc_info=True)
457
+ await send_link(message, links)
458
+ except Exception as e:
459
+ logger.error(f"Error in _actual_channel_receive_handler for message {message.id}: {e}", exc_info=True)
460
+
461
+ rl_user_id = None
462
+ if msg.sender_chat and msg.sender_chat.id:
463
+ rl_user_id = msg.sender_chat.id
464
+ elif msg.from_user:
465
+ rl_user_id = msg.from_user.id
466
+
467
+ if rl_user_id is None:
468
+ logger.debug(f"No identifiable user/channel for rate limiting for message {msg.id}. Skipping rate limit check and processing directly.")
469
+ await _actual_channel_receive_handler(bot, msg)
470
+ return
471
+
472
+ await handle_rate_limited_request(bot, msg, _actual_channel_receive_handler, rl_user_id=rl_user_id)
473
+
474
+
475
+ async def process_single(
476
+ bot: Client,
477
+ msg: Message,
478
+ file_msg: Message,
479
+ status_msg: Message,
480
+ shortener_val: bool,
481
+ original_request_msg: Optional[Message] = None,
482
  notification_msg: Optional[Message] = None
483
  ):
484
  try:
 
502
  return None
503
  links = await gen_links(stored_msg, shortener=shortener_val)
504
  canonical_reply_id = stored_msg.id
505
+ if notification_msg:
506
+ result = await safe_edit_message(
507
+ notification_msg,
508
+ MSG_LINKS.format(
509
+ file_name=links['media_name'],
510
+ file_size=links['media_size'],
511
+ download_link=links['online_link'],
512
+ stream_link=links['stream_link']
513
+ ),
514
+ parse_mode=enums.ParseMode.MARKDOWN,
515
+ disable_web_page_preview=True,
516
+ reply_markup=get_link_buttons(links)
517
+ )
518
+ if not result:
519
+ await send_link(msg, links)
520
  elif not original_request_msg:
521
+ await send_link(msg, links)
522
+ if msg.chat.type != enums.ChatType.PRIVATE and msg.from_user and not original_request_msg:
523
+ await send_dm_links(bot, msg.from_user.id, links, msg.chat.title or "the chat")
524
+ source_msg = original_request_msg if original_request_msg else msg
525
+ source_info = ""
526
+ source_id = 0
527
+ if source_msg.from_user:
528
+ source_info = source_msg.from_user.full_name
529
+ if not source_info:
530
+ source_info = f"@{source_msg.from_user.username}" if source_msg.from_user.username else "Unknown User"
531
+ source_id = source_msg.from_user.id
532
  elif source_msg.chat.type == enums.ChatType.CHANNEL:
533
  source_info = source_msg.chat.title or "Unknown Channel"
534
  source_id = source_msg.chat.id
 
552
  )
553
  if status_msg:
554
  await safe_delete_message(status_msg)
555
+ return links
556
+ except Exception as e:
557
+ logger.error(f"Error processing single file for message {file_msg.id}: {e}", exc_info=True)
558
+ if status_msg:
559
+ await safe_edit_message(status_msg, MSG_ERROR_PROCESSING_MEDIA)
560
+
561
+ await notify_own(bot, MSG_CRITICAL_ERROR.format(
562
+ error=str(e),
563
+ error_id=secrets.token_hex(6)
564
+ ))
565
+ return None
566
+
567
+
568
+ async def process_batch(
569
+ bot: Client,
570
+ msg: Message,
571
+ start_id: int,
572
+ count: int,
573
+ status_msg: Message,
574
+ shortener_val: bool,
575
+ notification_msg: Optional[Message] = None
576
+ ):
577
+ processed = 0
578
+ failed = 0
579
+ links_list = []
580
+ for batch_start in range(0, count, BATCH_SIZE):
581
+ batch_size = min(BATCH_SIZE, count - batch_start)
582
+ batch_ids = list(range(start_id + batch_start, start_id + batch_start + batch_size))
583
+ try:
584
+ try:
585
+ await status_msg.edit_text(
586
+ MSG_PROCESSING_BATCH.format(
587
+ batch_number=(batch_start // BATCH_SIZE) + 1,
588
+ total_batches=(count + BATCH_SIZE - 1) // BATCH_SIZE,
589
+ file_count=batch_size
590
+ )
591
+ )
592
+ except FloodWait as e:
593
+ await asyncio.sleep(e.value)
594
+ await status_msg.edit_text(
595
+ MSG_PROCESSING_BATCH.format(
596
+ batch_number=(batch_start // BATCH_SIZE) + 1,
597
+ total_batches=(count + BATCH_SIZE - 1) // BATCH_SIZE,
598
+ file_count=batch_size
599
+ )
600
+ )
601
+ except MessageNotModified:
602
+ pass
603
+ try:
604
+ try:
605
+ messages = await bot.get_messages(msg.chat.id, batch_ids)
606
+ except FloodWait as e:
607
+ await asyncio.sleep(e.value)
608
+ messages = await bot.get_messages(msg.chat.id, batch_ids)
609
+ if messages is None:
610
+ messages = []
611
+ except Exception as e:
612
+ logger.error(f"Error getting messages in batch: {e}", exc_info=True)
613
+ messages = []
614
+ for m in messages:
615
+ if m and m.media:
616
+ links = await process_single(bot, msg, m, None, shortener_val, original_request_msg=msg)
617
+ if links:
618
+ links_list.append(links['online_link'])
619
+ processed += 1
620
+ else:
621
+ failed += 1
622
+ else:
623
+ failed += 1
624
+ if (processed + failed) % BATCH_UPDATE_INTERVAL == 0 or (processed + failed) == count:
625
+ try:
626
+ try:
627
+ await status_msg.edit_text(
628
+ MSG_PROCESSING_STATUS.format(
629
+ processed=processed,
630
+ total=count,
631
+ failed=failed
632
+ )
633
+ )
634
+ except FloodWait as e:
635
+ await asyncio.sleep(e.value)
636
+ await status_msg.edit_text(
637
+ MSG_PROCESSING_STATUS.format(
638
+ processed=processed,
639
+ total=count,
640
+ failed=failed
641
+ )
642
+ )
643
+ except MessageNotModified:
644
+ pass
645
+ for i in range(0, len(links_list), LINK_CHUNK_SIZE):
646
+ chunk = links_list[i:i+LINK_CHUNK_SIZE]
647
+ chunk_text = MSG_BATCH_LINKS_READY.format(count=len(chunk)) + f"\n\n<code>{chr(10).join(chunk)}</code>"
648
+ try:
649
+ await msg.reply_text(
650
+ chunk_text,
651
+ quote=True,
652
+ disable_web_page_preview=True,
653
+ parse_mode=enums.ParseMode.HTML
654
+ )
655
+ except FloodWait as e:
656
+ await asyncio.sleep(e.value)
657
+ await msg.reply_text(
658
+ chunk_text,
659
+ quote=True,
660
+ disable_web_page_preview=True,
661
+ parse_mode=enums.ParseMode.HTML
662
+ )
663
+ if msg.chat.type != enums.ChatType.PRIVATE and msg.from_user:
664
+ try:
665
+ try:
666
+ await bot.send_message(
667
+ chat_id=msg.from_user.id,
668
+ text=MSG_DM_BATCH_PREFIX.format(chat_title=msg.chat.title or "the chat") + "\n" + chunk_text,
669
+ disable_web_page_preview=True,
670
+ parse_mode=enums.ParseMode.HTML
671
+ )
672
+ except FloodWait as e:
673
+ await asyncio.sleep(e.value)
674
+ await bot.send_message(
675
+ chat_id=msg.from_user.id,
676
+ text=MSG_DM_BATCH_PREFIX.format(chat_title=msg.chat.title or "the chat") + "\n" + chunk_text,
677
+ disable_web_page_preview=True,
678
+ parse_mode=enums.ParseMode.HTML
679
+ )
680
+ except Exception as e:
681
+ logger.error(f"Error sending DM in batch: {e}", exc_info=True)
682
+ await reply_user_err(msg, MSG_ERROR_DM_FAILED)
683
+ if i + LINK_CHUNK_SIZE < len(links_list):
684
+ await asyncio.sleep(MESSAGE_DELAY)
685
+ try:
686
+ await status_msg.edit_text(
687
+ MSG_PROCESSING_RESULT.format(
688
+ processed=processed,
689
+ total=count,
690
+ failed=failed
691
+ )
692
+ )
693
+ except FloodWait as e:
694
+ await asyncio.sleep(e.value)
695
+ await status_msg.edit_text(
696
+ MSG_PROCESSING_RESULT.format(
697
+ processed=processed,
698
+ total=count,
699
+ failed=failed
700
+ )
701
+ )
702
+ if notification_msg:
703
+ await safe_delete_message(notification_msg)
Thunder/bot/plugins/subscription.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Thunder/bot/plugins/subscription.py
2
+
3
+ import asyncio
4
+ import datetime
5
+ import urllib.parse
6
+ from typing import Optional
7
+
8
+ from pyrogram import Client, filters
9
+ from pyrogram.errors import FloodWait, MessageNotModified
10
+ from pyrogram.types import (CallbackQuery, InlineKeyboardButton,
11
+ InlineKeyboardMarkup, Message)
12
+
13
+ from Thunder.bot import StreamBot
14
+ from Thunder.utils.database import db
15
+ from Thunder.utils.logger import logger
16
+ from Thunder.utils.messages import (
17
+ MSG_PROOF_INSTRUCTION, MSG_SUB_APPROVED_USER, MSG_SUB_PLAN_ACTIVE,
18
+ MSG_SUB_PLAN_FREE, MSG_SUB_REJECTED_USER, MSG_TEMP_APPROVED_USER
19
+ )
20
+ from Thunder.vars import Var
21
+
22
+ # In-memory set of users currently prompted to submit payment proof
23
+ awaiting_proof = set()
24
+
25
+
26
+ def get_upi_link(user_id: int) -> str:
27
+ note = f"FlashFile_Sub_{user_id}"
28
+ params = {
29
+ "pa": Var.UPI_ID,
30
+ "pn": Var.UPI_NAME,
31
+ "am": str(Var.SUB_PRICE),
32
+ "cu": "INR",
33
+ "tn": note
34
+ }
35
+ return f"upi://pay?{urllib.parse.urlencode(params)}"
36
+
37
+
38
+ def get_plan_keyboard(user_id: int, file_id: Optional[str] = None) -> InlineKeyboardMarkup:
39
+ upi_link = get_upi_link(user_id)
40
+ buttons = [
41
+ [
42
+ InlineKeyboardButton(f"💳 Pay ₹{Var.SUB_PRICE} via UPI", url=upi_link)
43
+ ],
44
+ [
45
+ InlineKeyboardButton("📩 Submit Payment Proof", callback_data=f"proof_{user_id}")
46
+ ]
47
+ ]
48
+ if file_id:
49
+ buttons.append([
50
+ InlineKeyboardButton("🎟 Request 1-Time Stream Pass", callback_data=f"req_temp_{user_id}_{file_id}")
51
+ ])
52
+ buttons.append([
53
+ InlineKeyboardButton("❌ Close", callback_data="close_sub")
54
+ ])
55
+ return InlineKeyboardMarkup(buttons)
56
+
57
+
58
+ @StreamBot.on_message(filters.command(["plan", "buy", "sub"]) & filters.private)
59
+ async def plan_command_handler(client: Client, message: Message):
60
+ user_id = message.from_user.id
61
+ user_name = message.from_user.first_name or "User"
62
+
63
+ if not Var.PAYMENT_ENABLED:
64
+ await message.reply_text(
65
+ "⚡ **Flash File Service Status**\n\n"
66
+ "All streaming and download links are currently **100% Free**!\n"
67
+ "No active subscription or payment is required.",
68
+ quote=True
69
+ )
70
+ return
71
+
72
+ sub = await db.get_subscription(user_id)
73
+ now = datetime.datetime.now(datetime.timezone.utc)
74
+
75
+ if sub and sub.get("status") == "active" and sub.get("expires_at") and sub["expires_at"] > now:
76
+ expires = sub["expires_at"]
77
+ remaining = max(1, (expires - now).days)
78
+ text = MSG_SUB_PLAN_ACTIVE.format(
79
+ user_name=user_name,
80
+ user_id=user_id,
81
+ expiry_date=expires.strftime("%Y-%m-%d %H:%M UTC"),
82
+ remaining_days=remaining
83
+ )
84
+ keyboard = InlineKeyboardMarkup([
85
+ [InlineKeyboardButton("➕ Extend Subscription (₹10)", callback_data=f"sub_pay_{user_id}")],
86
+ [InlineKeyboardButton("❌ Close", callback_data="close_sub")]
87
+ ])
88
+ else:
89
+ text = MSG_SUB_PLAN_FREE.format(
90
+ price=Var.SUB_PRICE,
91
+ upi_id=Var.UPI_ID
92
+ )
93
+ keyboard = get_plan_keyboard(user_id)
94
+
95
+ await message.reply_text(text, reply_markup=keyboard, quote=True)
96
+
97
+
98
+ @StreamBot.on_message(filters.command("addsub") & filters.private)
99
+ async def addsub_command_handler(client: Client, message: Message):
100
+ sender_id = message.from_user.id
101
+ is_owner = sender_id == Var.OWNER_ID
102
+ is_admin = hasattr(Var, "ADMINS") and sender_id in Var.ADMINS
103
+
104
+ if not (is_owner or is_admin):
105
+ return
106
+
107
+ parts = message.text.strip().split()
108
+ if len(parts) < 2:
109
+ await message.reply_text("Usage: `/addsub <user_id> [days]`", quote=True)
110
+ return
111
+
112
+ try:
113
+ target_id = int(parts[1])
114
+ days = int(parts[2]) if len(parts) > 2 else Var.SUB_DURATION_DAYS
115
+ except ValueError:
116
+ await message.reply_text("Invalid user ID or days number.", quote=True)
117
+ return
118
+
119
+ doc = await db.add_subscription(target_id, days=days, plan="admin_granted")
120
+ expiry = doc["expires_at"].strftime("%Y-%m-%d %H:%M UTC")
121
+
122
+ await message.reply_text(
123
+ f"✅ Subscription granted to `{target_id}` for {days} days.\n"
124
+ f"Valid until: `{expiry}`",
125
+ quote=True
126
+ )
127
+
128
+ try:
129
+ await client.send_message(
130
+ target_id,
131
+ MSG_SUB_APPROVED_USER.format(expiry_date=expiry)
132
+ )
133
+ except Exception as e:
134
+ logger.warning(f"Could not notify user {target_id} of granted subscription: {e}")
135
+
136
+
137
+ @StreamBot.on_message(filters.command("remsub") & filters.private)
138
+ async def remsub_command_handler(client: Client, message: Message):
139
+ sender_id = message.from_user.id
140
+ is_owner = sender_id == Var.OWNER_ID
141
+ is_admin = hasattr(Var, "ADMINS") and sender_id in Var.ADMINS
142
+
143
+ if not (is_owner or is_admin):
144
+ return
145
+
146
+ parts = message.text.strip().split()
147
+ if len(parts) < 2:
148
+ await message.reply_text("Usage: `/remsub <user_id>`", quote=True)
149
+ return
150
+
151
+ try:
152
+ target_id = int(parts[1])
153
+ except ValueError:
154
+ await message.reply_text("Invalid user ID.", quote=True)
155
+ return
156
+
157
+ success = await db.remove_subscription(target_id)
158
+ if success:
159
+ await message.reply_text(f"✅ Subscription revoked for `{target_id}`.", quote=True)
160
+ else:
161
+ await message.reply_text(f"Could not revoke or no active subscription found for `{target_id}`.", quote=True)
162
+
163
+
164
+ @StreamBot.on_message(filters.command("subscribers") & filters.private)
165
+ async def subscribers_command_handler(client: Client, message: Message):
166
+ sender_id = message.from_user.id
167
+ is_owner = sender_id == Var.OWNER_ID
168
+ is_admin = hasattr(Var, "ADMINS") and sender_id in Var.ADMINS
169
+
170
+ if not (is_owner or is_admin):
171
+ return
172
+
173
+ count = await db.get_active_subscribers_count()
174
+ subscribers = await db.get_all_subscribers(limit=25)
175
+
176
+ text = f"👑 **Active Subscribers:** `{count}`\n\n"
177
+ if subscribers:
178
+ for idx, sub in enumerate(subscribers, 1):
179
+ exp = sub["expires_at"].strftime("%Y-%m-%d")
180
+ text += f"{idx}. `{sub['user_id']}` — Expiry: `{exp}` ({sub.get('plan', 'monthly')})\n"
181
+ else:
182
+ text += "No active subscribers found."
183
+
184
+ await message.reply_text(text, quote=True)
185
+
186
+
187
+ @StreamBot.on_callback_query(filters.regex(r"^close_sub$"))
188
+ async def close_sub_callback(client: Client, query: CallbackQuery):
189
+ await query.answer()
190
+ try:
191
+ await query.message.delete()
192
+ except Exception:
193
+ pass
194
+
195
+
196
+ @StreamBot.on_callback_query(filters.regex(r"^sub_pay_(\d+)$"))
197
+ async def sub_pay_callback(client: Client, query: CallbackQuery):
198
+ user_id = int(query.matches[0].group(1))
199
+ upi_link = get_upi_link(user_id)
200
+ text = (
201
+ "💳 **Pay ₹10 via UPI**\n\n"
202
+ f"**UPI ID:** `{Var.UPI_ID}`\n"
203
+ f"**Name:** `{Var.UPI_NAME}`\n"
204
+ f"**Amount:** `₹{Var.SUB_PRICE}`\n\n"
205
+ "Click the button below to launch Google Pay / PhonePe / Paytm / BHIM directly:"
206
+ )
207
+ keyboard = InlineKeyboardMarkup([
208
+ [InlineKeyboardButton(f"🚀 Open UPI App (₹{Var.SUB_PRICE})", url=upi_link)],
209
+ [InlineKeyboardButton("📩 Submit Proof / Tx ID", callback_data=f"proof_{user_id}")],
210
+ [InlineKeyboardButton("⬅️ Back", callback_data=f"plan_info_{user_id}")]
211
+ ])
212
+ await query.answer()
213
+ try:
214
+ await query.message.edit_text(text, reply_markup=keyboard)
215
+ except MessageNotModified:
216
+ pass
217
+
218
+
219
+ @StreamBot.on_callback_query(filters.regex(r"^plan_info_(\d+)$"))
220
+ async def plan_info_callback(client: Client, query: CallbackQuery):
221
+ user_id = int(query.matches[0].group(1))
222
+ text = MSG_SUB_PLAN_FREE.format(price=Var.SUB_PRICE, upi_id=Var.UPI_ID)
223
+ keyboard = get_plan_keyboard(user_id)
224
+ await query.answer()
225
+ try:
226
+ await query.message.edit_text(text, reply_markup=keyboard)
227
+ except MessageNotModified:
228
+ pass
229
+
230
+
231
+ @StreamBot.on_callback_query(filters.regex(r"^proof_(\d+)$"))
232
+ async def proof_callback(client: Client, query: CallbackQuery):
233
+ user_id = int(query.matches[0].group(1))
234
+ awaiting_proof.add(user_id)
235
+ await query.answer("Please send your payment screenshot or UTR number.", show_alert=True)
236
+ await query.message.reply_text(MSG_PROOF_INSTRUCTION, quote=True)
237
+
238
+
239
+ @StreamBot.on_callback_query(filters.regex(r"^req_temp_(\d+)_(.+)$"))
240
+ async def req_temp_callback(client: Client, query: CallbackQuery):
241
+ user_id = int(query.matches[0].group(1))
242
+ file_id = query.matches[0].group(2)
243
+ user_name = query.from_user.first_name or "User"
244
+
245
+ await query.answer("Submitting temporary stream pass request to admin...", show_alert=False)
246
+
247
+ req_id = await db.create_sub_request(
248
+ user_id=user_id,
249
+ user_name=user_name,
250
+ req_type="temp_pass",
251
+ file_id=file_id
252
+ )
253
+
254
+ admin_target = Var.ADMIN_CHANNEL or Var.OWNER_ID
255
+ if admin_target:
256
+ admin_card = (
257
+ "🎟 **New 1-Time Stream Pass Request**\n\n"
258
+ f"👤 **User:** {user_name} (`{user_id}`)\n"
259
+ f"📄 **File Unique ID:** `{file_id}`\n"
260
+ f"🆔 **Request ID:** `{req_id}`\n\n"
261
+ "Choose action below:"
262
+ )
263
+ keyboard = InlineKeyboardMarkup([
264
+ [
265
+ InlineKeyboardButton("🎟 Approve Temp Pass (24h)", callback_data=f"approve_temp_{req_id}"),
266
+ InlineKeyboardButton("👑 Approve 30-Day Sub", callback_data=f"approve_sub_{req_id}")
267
+ ],
268
+ [
269
+ InlineKeyboardButton("❌ Reject", callback_data=f"reject_sub_{req_id}")
270
+ ]
271
+ ])
272
+ try:
273
+ await client.send_message(admin_target, admin_card, reply_markup=keyboard)
274
+ except Exception as e:
275
+ logger.error(f"Failed to notify admin target {admin_target} of temp pass request: {e}")
276
+
277
+ await query.message.reply_text(
278
+ "⏳ **Request Submitted!**\n\n"
279
+ "Your temporary pass request has been sent to the admin team for approval. "
280
+ "You will receive a notification as soon as it is reviewed.",
281
+ quote=True
282
+ )
283
+
284
+
285
+ @StreamBot.on_callback_query(filters.regex(r"^approve_sub_([a-f0-9]+)$"))
286
+ async def approve_sub_callback(client: Client, query: CallbackQuery):
287
+ admin_id = query.from_user.id
288
+ if not (admin_id == Var.OWNER_ID or (hasattr(Var, "ADMINS") and admin_id in Var.ADMINS)):
289
+ await query.answer("Unauthorized", show_alert=True)
290
+ return
291
+
292
+ req_id = query.matches[0].group(1)
293
+ req = await db.get_sub_request(req_id)
294
+ if not req or req.get("status") != "pending":
295
+ await query.answer("This request has already been processed.", show_alert=True)
296
+ return
297
+
298
+ user_id = req["user_id"]
299
+ doc = await db.add_subscription(user_id, days=Var.SUB_DURATION_DAYS, tx_ref=req.get("tx_ref", ""))
300
+ await db.update_sub_request_status(req_id, "approved_sub", admin_id)
301
+
302
+ admin_name = query.from_user.first_name or "Admin"
303
+ expiry = doc["expires_at"].strftime("%Y-%m-%d %H:%M UTC")
304
+
305
+ await query.answer("30-Day Monthly Subscription Approved!", show_alert=True)
306
+ try:
307
+ await query.message.edit_text(
308
+ f"{query.message.text}\n\n"
309
+ f"✅ **APPROVED (30-Day Plan)** by {admin_name} on {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M')}"
310
+ )
311
+ except MessageNotModified:
312
+ pass
313
+
314
+ try:
315
+ await client.send_message(
316
+ user_id,
317
+ MSG_SUB_APPROVED_USER.format(expiry_date=expiry)
318
+ )
319
+ except Exception as e:
320
+ logger.warning(f"Could not notify user {user_id} of approved subscription: {e}")
321
+
322
+
323
+ @StreamBot.on_callback_query(filters.regex(r"^approve_temp_([a-f0-9]+)$"))
324
+ async def approve_temp_callback(client: Client, query: CallbackQuery):
325
+ admin_id = query.from_user.id
326
+ if not (admin_id == Var.OWNER_ID or (hasattr(Var, "ADMINS") and admin_id in Var.ADMINS)):
327
+ await query.answer("Unauthorized", show_alert=True)
328
+ return
329
+
330
+ req_id = query.matches[0].group(1)
331
+ req = await db.get_sub_request(req_id)
332
+ if not req or req.get("status") != "pending":
333
+ await query.answer("This request has already been processed.", show_alert=True)
334
+ return
335
+
336
+ user_id = req["user_id"]
337
+ file_id = req.get("file_id") or "any"
338
+ await db.add_temp_pass(user_id, file_id, hours=24)
339
+ await db.update_sub_request_status(req_id, "approved_temp", admin_id)
340
+
341
+ admin_name = query.from_user.first_name or "Admin"
342
+
343
+ await query.answer("1-Time Temporary Stream Pass Approved!", show_alert=True)
344
+ try:
345
+ await query.message.edit_text(
346
+ f"{query.message.text}\n\n"
347
+ f"🎟 **APPROVED (1-Time Pass)** by {admin_name} on {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M')}"
348
+ )
349
+ except MessageNotModified:
350
+ pass
351
+
352
+ try:
353
+ await client.send_message(
354
+ user_id,
355
+ MSG_TEMP_APPROVED_USER
356
+ )
357
+ except Exception as e:
358
+ logger.warning(f"Could not notify user {user_id} of approved temp pass: {e}")
359
+
360
+
361
+ @StreamBot.on_callback_query(filters.regex(r"^reject_sub_([a-f0-9]+)$"))
362
+ async def reject_sub_callback(client: Client, query: CallbackQuery):
363
+ admin_id = query.from_user.id
364
+ if not (admin_id == Var.OWNER_ID or (hasattr(Var, "ADMINS") and admin_id in Var.ADMINS)):
365
+ await query.answer("Unauthorized", show_alert=True)
366
+ return
367
+
368
+ req_id = query.matches[0].group(1)
369
+ req = await db.get_sub_request(req_id)
370
+ if not req or req.get("status") != "pending":
371
+ await query.answer("This request has already been processed.", show_alert=True)
372
+ return
373
+
374
+ user_id = req["user_id"]
375
+ await db.update_sub_request_status(req_id, "rejected", admin_id)
376
+
377
+ admin_name = query.from_user.first_name or "Admin"
378
+ await query.answer("Request Rejected.", show_alert=True)
379
+ try:
380
+ await query.message.edit_text(
381
+ f"{query.message.text}\n\n"
382
+ f"❌ **REJECTED** by {admin_name} on {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M')}"
383
+ )
384
+ except MessageNotModified:
385
+ pass
386
+
387
+ try:
388
+ await client.send_message(
389
+ user_id,
390
+ MSG_SUB_REJECTED_USER
391
+ )
392
+ except Exception as e:
393
+ logger.warning(f"Could not notify user {user_id} of rejected request: {e}")
394
+
395
+
396
+ @StreamBot.on_message(filters.private & (filters.photo | filters.document | filters.text) & ~filters.command(["start", "help", "about", "plan", "buy", "sub"]))
397
+ async def proof_receiver_handler(client: Client, message: Message):
398
+ user_id = message.from_user.id if message.from_user else None
399
+ if not user_id:
400
+ return
401
+
402
+ # Check if user is actively awaiting proof or if message looks like a payment proof
403
+ has_text_utr = False
404
+ tx_ref = ""
405
+ if message.text:
406
+ text = message.text.strip()
407
+ # Common UTR / UPI transaction references (12 digits or words like UPI, UTR, ref)
408
+ if len(text) >= 8 and (text.isdigit() or any(k in text.lower() for k in ["utr", "upi", "txn", "ref", "paid"])):
409
+ has_text_utr = True
410
+ tx_ref = text
411
+
412
+ is_photo_proof = bool(message.photo or (message.document and message.document.mime_type and "image" in message.document.mime_type))
413
+
414
+ if user_id not in awaiting_proof and not (has_text_utr and len(tx_ref) == 12 and tx_ref.isdigit()):
415
+ return
416
+
417
+ awaiting_proof.discard(user_id)
418
+ user_name = message.from_user.first_name or "User"
419
+
420
+ photo_file_id = message.photo.file_id if message.photo else (message.document.file_id if message.document else None)
421
+ caption_text = message.caption or tx_ref or "Payment Proof attached"
422
+
423
+ req_id = await db.create_sub_request(
424
+ user_id=user_id,
425
+ user_name=user_name,
426
+ req_type="monthly_proof",
427
+ tx_ref=caption_text,
428
+ proof_file_id=photo_file_id
429
+ )
430
+
431
+ admin_target = Var.ADMIN_CHANNEL or Var.OWNER_ID
432
+ if admin_target:
433
+ card_text = (
434
+ "💰 **New Payment Proof Received**\n\n"
435
+ f"👤 **User:** {user_name} (`{user_id}`)\n"
436
+ f"📝 **Ref / Note:** `{caption_text}`\n"
437
+ f"🆔 **Request ID:** `{req_id}`\n\n"
438
+ "Review payment and choose action:"
439
+ )
440
+ keyboard = InlineKeyboardMarkup([
441
+ [
442
+ InlineKeyboardButton("✅ Approve 30-Day Sub", callback_data=f"approve_sub_{req_id}"),
443
+ InlineKeyboardButton("🎟 Approve Temp Pass", callback_data=f"approve_temp_{req_id}")
444
+ ],
445
+ [
446
+ InlineKeyboardButton("❌ Reject", callback_data=f"reject_sub_{req_id}")
447
+ ]
448
+ ])
449
+
450
+ try:
451
+ if photo_file_id:
452
+ await client.send_photo(
453
+ admin_target,
454
+ photo=photo_file_id,
455
+ caption=card_text,
456
+ reply_markup=keyboard
457
+ )
458
+ else:
459
+ await client.send_message(
460
+ admin_target,
461
+ card_text,
462
+ reply_markup=keyboard
463
+ )
464
+ except Exception as e:
465
+ logger.error(f"Failed to forward proof to admin {admin_target}: {e}")
466
+
467
+ await message.reply_text(
468
+ "✅ **Payment Proof Received!**\n\n"
469
+ "Your transaction has been submitted to admin for verification.\n"
470
+ "Your pass will be activated as soon as it is confirmed.",
471
+ quote=True
472
+ )
Thunder/utils/commands.py CHANGED
@@ -1,38 +1,43 @@
1
- from pyrogram.types import BotCommand
2
-
3
- from Thunder.bot import StreamBot
4
- from Thunder.utils.logger import logger
5
- from Thunder.vars import Var
6
-
7
- def get_commands():
8
- command_descriptions = {
9
- "start": "Start the bot and get a welcome message",
10
- "link": "(Group) Generate a direct link for a file or batch",
11
- "dc": "Retrieve the data center (DC) information of a user or file",
12
- "ping": "Check the bot's status and response time",
13
- "about": "Get information about the bot",
14
- "help": "Show help and usage instructions",
15
- "status": "(Admin) View bot details and current workload",
16
- "stats": "(Admin) View usage statistics and resource consumption",
17
- "broadcast": "(Admin) Send a message to all users",
18
- "ban": "(Admin) Ban a user",
19
- "unban": "(Admin) Unban a user",
20
- "log": "(Admin) Send bot logs",
21
- "restart": "(Admin) Update and restart the bot",
22
- "shell": "(Admin) Execute a shell command",
23
- "speedtest": "(Admin) Run network speed test",
24
- "users": "(Admin) Show the total number of users",
25
- "authorize": "(Admin) Grant permanent access to a user",
26
- "deauthorize": "(Admin) Remove permanent access from a user",
27
- "listauth": "(Admin) List all authorized users"
28
- }
29
- return [BotCommand(name, desc) for name, desc in command_descriptions.items()]
30
-
31
- async def set_commands():
32
- if Var.SET_COMMANDS:
33
- try:
34
- commands = get_commands()
35
- if commands:
36
- await StreamBot.set_bot_commands(commands)
37
- except Exception as e:
38
- logger.error(f"Failed to set bot commands: {e}", exc_info=True)
 
 
 
 
 
 
1
+ from pyrogram.types import BotCommand
2
+
3
+ from Thunder.bot import StreamBot
4
+ from Thunder.utils.logger import logger
5
+ from Thunder.vars import Var
6
+
7
+ def get_commands():
8
+ command_descriptions = {
9
+ "start": "Start the bot and get a welcome message",
10
+ "link": "(Group) Generate a direct link for a file or batch",
11
+ "dc": "Retrieve the data center (DC) information of a user or file",
12
+ "ping": "Check the bot's status and response time",
13
+ "about": "Get information about the bot",
14
+ "help": "Show help and usage instructions",
15
+ "status": "(Admin) View bot details and current workload",
16
+ "stats": "(Admin) View usage statistics and resource consumption",
17
+ "broadcast": "(Admin) Send a message to all users",
18
+ "ban": "(Admin) Ban a user",
19
+ "unban": "(Admin) Unban a user",
20
+ "log": "(Admin) Send bot logs",
21
+ "restart": "(Admin) Update and restart the bot",
22
+ "shell": "(Admin) Execute a shell command",
23
+ "speedtest": "(Admin) Run network speed test",
24
+ "users": "(Admin) Show the total number of users",
25
+ "authorize": "(Admin) Grant permanent access to a user",
26
+ "deauthorize": "(Admin) Remove permanent access from a user",
27
+ "listauth": "(Admin) List all authorized users",
28
+ "plan": "View subscription status or purchase monthly plan",
29
+ "buy": "Purchase ₹10/month subscription pass",
30
+ "addsub": "(Admin) Grant subscription to a user",
31
+ "remsub": "(Admin) Revoke subscription from a user",
32
+ "subscribers": "(Admin) View active subscriber count"
33
+ }
34
+ return [BotCommand(name, desc) for name, desc in command_descriptions.items()]
35
+
36
+ async def set_commands():
37
+ if Var.SET_COMMANDS:
38
+ try:
39
+ commands = get_commands()
40
+ if commands:
41
+ await StreamBot.set_bot_commands(commands)
42
+ except Exception as e:
43
+ logger.error(f"Failed to set bot commands: {e}", exc_info=True)
Thunder/utils/database.py CHANGED
@@ -1,453 +1,625 @@
1
- # Thunder/utils/database.py
2
-
3
- import datetime
4
- from typing import Any, Dict, Optional
5
- from pymongo import AsyncMongoClient
6
- from pymongo.asynchronous.collection import AsyncCollection
7
- from pymongo.errors import DuplicateKeyError
8
- from Thunder.vars import Var
9
- from Thunder.utils.logger import logger
10
-
11
- class Database:
12
- def __init__(self, uri: str, database_name: str, *args, **kwargs):
13
- self._client = AsyncMongoClient(uri, *args, **kwargs)
14
- self.db = self._client[database_name]
15
- self.col: AsyncCollection = self.db.users
16
- self.banned_users_col: AsyncCollection = self.db.banned_users
17
- self.banned_channels_col: AsyncCollection = self.db.banned_channels
18
- self.token_col: AsyncCollection = self.db.tokens
19
- self.authorized_users_col: AsyncCollection = self.db.authorized_users
20
- self.restart_message_col: AsyncCollection = self.db.restart_message
21
- self.files_col: AsyncCollection = self.db.files
22
- self.file_ingest_locks_col: AsyncCollection = self.db.file_ingest_locks
23
-
24
- async def _deduplicate_users(self) -> None:
25
- pipeline = [
26
- {"$sort": {"join_date": 1}},
27
- {"$group": {"_id": "$id", "doc_id": {"$first": "$_id"}}},
28
- {"$project": {"_id": "$doc_id"}}
29
- ]
30
- keep_ids = []
31
- async for doc in self.col.aggregate(pipeline):
32
- keep_ids.append(doc["_id"])
33
- if keep_ids:
34
- result = await self.col.delete_many({"_id": {"$nin": keep_ids}})
35
- if result.deleted_count > 0:
36
- logger.warning(f"Deduplicated {result.deleted_count} duplicate user documents.")
37
-
38
- async def ensure_indexes(self, *, raise_on_error: bool = True) -> bool:
39
- try:
40
- await self.banned_users_col.create_index("user_id", unique=True)
41
- await self.banned_channels_col.create_index("channel_id", unique=True)
42
- await self.token_col.create_index("token", unique=True)
43
- await self.authorized_users_col.create_index("user_id", unique=True)
44
- try:
45
- await self.col.create_index("id", unique=True)
46
- except DuplicateKeyError:
47
- logger.warning("Duplicate users found, deduplicating...")
48
- await self._deduplicate_users()
49
- await self.col.create_index("id", unique=True)
50
- await self.token_col.create_index("expires_at", expireAfterSeconds=0)
51
- await self.token_col.create_index("activated")
52
- await self.restart_message_col.create_index("message_id", unique=True)
53
- await self.restart_message_col.create_index("timestamp", expireAfterSeconds=3600)
54
- await self.files_col.create_index("file_unique_id", unique=True)
55
- await self.files_col.create_index("public_hash", unique=True)
56
- await self.files_col.create_index("canonical_message_id", unique=True)
57
- await self.files_col.create_index("created_at")
58
- await self.files_col.create_index("last_seen_at")
59
- await self.file_ingest_locks_col.create_index("expires_at", expireAfterSeconds=0)
60
-
61
- logger.debug("Database indexes ensured.")
62
- return True
63
- except Exception as e:
64
- logger.error(f"Error in ensure_indexes: {e}", exc_info=True)
65
- if raise_on_error:
66
- raise
67
- return False
68
-
69
- def new_user(self, user_id: int) -> dict:
70
- try:
71
- return {
72
- 'id': user_id,
73
- 'join_date': datetime.datetime.now(datetime.timezone.utc)
74
- }
75
- except Exception as e:
76
- logger.error(f"Error in new_user for user {user_id}: {e}", exc_info=True)
77
- raise
78
-
79
- async def add_user(self, user_id: int) -> bool:
80
- try:
81
- result = await self.col.update_one(
82
- {'id': user_id},
83
- {'$setOnInsert': self.new_user(user_id)},
84
- upsert=True
85
- )
86
- if result.upserted_id:
87
- logger.debug(f"Added new user {user_id} to database.")
88
- return True
89
- return False
90
- except Exception as e:
91
- logger.error(f"Error in add_user for user {user_id}: {e}", exc_info=True)
92
- raise
93
-
94
-
95
- async def is_user_exist(self, user_id: int) -> bool:
96
- """Read-only existence check. For user registration, use add_user() instead."""
97
- try:
98
- user = await self.col.find_one({'id': user_id}, {'_id': 1})
99
- return bool(user)
100
- except Exception as e:
101
- logger.error(f"Error in is_user_exist for user {user_id}: {e}", exc_info=True)
102
- raise
103
-
104
- async def total_users_count(self) -> int:
105
- try:
106
- return await self.col.count_documents({})
107
- except Exception as e:
108
- logger.error(f"Error in total_users_count: {e}", exc_info=True)
109
- return 0
110
-
111
- async def get_authorized_users_count(self) -> int:
112
- try:
113
- return await self.authorized_users_col.count_documents({})
114
- except Exception as e:
115
- logger.error(f"Error in get_authorized_users_count: {e}", exc_info=True)
116
- return 0
117
-
118
- async def get_regular_users_count(self) -> int:
119
- try:
120
- auth_ids = await self.authorized_users_col.distinct("user_id")
121
- return await self.col.count_documents({"id": {"$nin": auth_ids}})
122
- except Exception as e:
123
- logger.error(f"Error in get_regular_users_count: {e}", exc_info=True)
124
- return 0
125
-
126
- async def get_all_users(self):
127
- try:
128
- return self.col.find({})
129
- except Exception as e:
130
- logger.error(f"Error in get_all_users: {e}", exc_info=True)
131
- return self.col.find({"_id": {"$exists": False}})
132
-
133
- async def get_authorized_users_cursor(self):
134
- try:
135
- return self.authorized_users_col.find({})
136
- except Exception as e:
137
- logger.error(f"Error in get_authorized_users_cursor: {e}", exc_info=True)
138
- return self.authorized_users_col.find({"_id": {"$exists": False}})
139
-
140
- async def get_regular_users_cursor(self):
141
- try:
142
- auth_ids = await self.authorized_users_col.distinct("user_id")
143
- return self.col.find({"id": {"$nin": auth_ids}})
144
- except Exception as e:
145
- logger.error(f"Error in get_regular_users_cursor: {e}", exc_info=True)
146
- return self.col.find({"_id": {"$exists": False}})
147
-
148
- async def delete_user(self, user_id: int):
149
- try:
150
- await self.col.delete_one({'id': user_id})
151
- logger.debug(f"Deleted user {user_id}.")
152
- except Exception as e:
153
- logger.error(f"Error in delete_user for user {user_id}: {e}", exc_info=True)
154
- raise
155
-
156
-
157
- async def add_banned_user(
158
- self, user_id: int, banned_by: Optional[int] = None,
159
- reason: Optional[str] = None
160
- ):
161
- try:
162
- ban_data = {
163
- "user_id": user_id,
164
- "banned_at": datetime.datetime.now(datetime.timezone.utc),
165
- "banned_by": banned_by,
166
- "reason": reason
167
- }
168
- await self.banned_users_col.update_one(
169
- {"user_id": user_id},
170
- {"$set": ban_data},
171
- upsert=True
172
- )
173
- logger.debug(f"Added/Updated banned user {user_id}. Reason: {reason}")
174
- except Exception as e:
175
- logger.error(f"Error in add_banned_user for user {user_id}: {e}", exc_info=True)
176
- raise
177
-
178
- async def remove_banned_user(self, user_id: int) -> bool:
179
- try:
180
- result = await self.banned_users_col.delete_one({"user_id": user_id})
181
- if result.deleted_count > 0:
182
- logger.debug(f"Removed banned user {user_id}.")
183
- return True
184
- return False
185
- except Exception as e:
186
- logger.error(f"Error in remove_banned_user for user {user_id}: {e}", exc_info=True)
187
- return False
188
-
189
- async def is_user_banned(self, user_id: int) -> Optional[Dict[str, Any]]:
190
- try:
191
- return await self.banned_users_col.find_one({"user_id": user_id})
192
- except Exception as e:
193
- logger.error(f"Error in is_user_banned for user {user_id}: {e}", exc_info=True)
194
- return None
195
-
196
- async def add_banned_channel(
197
- self, channel_id: int, banned_by: Optional[int] = None,
198
- reason: Optional[str] = None
199
- ):
200
- try:
201
- ban_data = {
202
- "channel_id": channel_id,
203
- "banned_at": datetime.datetime.now(datetime.timezone.utc),
204
- "banned_by": banned_by,
205
- "reason": reason
206
- }
207
- await self.banned_channels_col.update_one(
208
- {"channel_id": channel_id},
209
- {"$set": ban_data},
210
- upsert=True
211
- )
212
- logger.debug(f"Added/Updated banned channel {channel_id}. Reason: {reason}")
213
- except Exception as e:
214
- logger.error(f"Error in add_banned_channel for channel {channel_id}: {e}", exc_info=True)
215
- raise
216
-
217
- async def remove_banned_channel(self, channel_id: int) -> bool:
218
- try:
219
- result = await self.banned_channels_col.delete_one({"channel_id": channel_id})
220
- if result.deleted_count > 0:
221
- logger.debug(f"Removed banned channel {channel_id}.")
222
- return True
223
- return False
224
- except Exception as e:
225
- logger.error(f"Error in remove_banned_channel for channel {channel_id}: {e}", exc_info=True)
226
- return False
227
-
228
- async def is_channel_banned(self, channel_id: int) -> Optional[Dict[str, Any]]:
229
- try:
230
- return await self.banned_channels_col.find_one({"channel_id": channel_id})
231
- except Exception as e:
232
- logger.error(f"Error in is_channel_banned for channel {channel_id}: {e}", exc_info=True)
233
- return None
234
-
235
- async def save_main_token(self, user_id: int, token_value: str, expires_at: datetime.datetime, created_at: datetime.datetime, activated: bool) -> None:
236
- try:
237
- await self.token_col.update_one(
238
- {"user_id": user_id, "token": token_value},
239
- {"$set": {
240
- "expires_at": expires_at,
241
- "created_at": created_at,
242
- "activated": activated
243
- }
244
- },
245
- upsert=True
246
- )
247
- logger.debug(f"Saved main token {token_value} for user {user_id} with activated status {activated}.")
248
- except Exception as e:
249
- logger.error(f"Error saving main token for user {user_id}: {e}", exc_info=True)
250
- raise
251
-
252
-
253
- async def add_restart_message(self, message_id: int, chat_id: int) -> None:
254
- try:
255
- await self.restart_message_col.insert_one({
256
- "message_id": message_id,
257
- "chat_id": chat_id,
258
- "timestamp": datetime.datetime.now(datetime.timezone.utc)
259
- })
260
- logger.debug(f"Added restart message {message_id} for chat {chat_id}.")
261
- except Exception as e:
262
- logger.error(f"Error adding restart message {message_id}: {e}", exc_info=True)
263
-
264
- async def get_restart_message(self) -> Optional[Dict[str, Any]]:
265
- try:
266
- return await self.restart_message_col.find_one(sort=[("timestamp", -1)])
267
- except Exception as e:
268
- logger.error(f"Error getting restart message: {e}", exc_info=True)
269
- return None
270
-
271
- async def delete_restart_message(self, message_id: int) -> None:
272
- try:
273
- await self.restart_message_col.delete_one({"message_id": message_id})
274
- logger.debug(f"Deleted restart message {message_id}.")
275
- except Exception as e:
276
- logger.error(f"Error deleting restart message {message_id}: {e}", exc_info=True)
277
-
278
- async def is_user_authorized(self, user_id: int) -> bool:
279
- try:
280
- user = await self.authorized_users_col.find_one({'user_id': user_id}, {'_id': 1})
281
- return bool(user)
282
- except Exception as e:
283
- logger.error(f"Error in is_user_authorized for user {user_id}: {e}", exc_info=True)
284
- return False
285
-
286
- async def get_file_by_unique_id(self, file_unique_id: str) -> Optional[Dict[str, Any]]:
287
- try:
288
- return await self.files_col.find_one({"file_unique_id": file_unique_id})
289
- except Exception as e:
290
- logger.error(f"Error getting file by unique_id {file_unique_id}: {e}", exc_info=True)
291
- return None
292
-
293
- async def get_file_by_hash(
294
- self,
295
- public_hash: str,
296
- *,
297
- raise_on_error: bool = True
298
- ) -> Optional[Dict[str, Any]]:
299
- try:
300
- return await self.files_col.find_one({"public_hash": public_hash})
301
- except Exception as e:
302
- logger.error(f"Error getting file by hash {public_hash}: {e}", exc_info=True)
303
- if raise_on_error:
304
- raise
305
- return None
306
-
307
- async def get_file_by_message_id(self, canonical_message_id: int) -> Optional[Dict[str, Any]]:
308
- try:
309
- return await self.files_col.find_one({"canonical_message_id": canonical_message_id})
310
- except Exception as e:
311
- logger.error(
312
- f"Error getting file by message_id {canonical_message_id}: {e}",
313
- exc_info=True
314
- )
315
- return None
316
-
317
- async def create_file_record(self, file_record: Dict[str, Any]) -> None:
318
- try:
319
- await self.files_col.insert_one(file_record)
320
- except Exception as e:
321
- logger.error(
322
- f"Error creating canonical file record for {file_record.get('file_unique_id')}: {e}",
323
- exc_info=True
324
- )
325
- raise
326
-
327
- async def replace_file_record(self, file_record: Dict[str, Any]) -> None:
328
- try:
329
- await self.files_col.replace_one(
330
- {"file_unique_id": file_record["file_unique_id"]},
331
- file_record,
332
- upsert=True
333
- )
334
- except Exception as e:
335
- logger.error(
336
- f"Error replacing canonical file record for {file_record.get('file_unique_id')}: {e}",
337
- exc_info=True
338
- )
339
- raise
340
-
341
- async def touch_file_record(
342
- self,
343
- public_hash: str,
344
- *,
345
- reused: bool = False,
346
- raise_on_error: bool = False
347
- ) -> bool:
348
- try:
349
- update_doc: Dict[str, Any] = {
350
- "$set": {"last_seen_at": datetime.datetime.now(datetime.timezone.utc)},
351
- "$inc": {"seen_count": 1}
352
- }
353
- if reused:
354
- update_doc["$inc"]["reuse_count"] = 1
355
- await self.files_col.update_one({"public_hash": public_hash}, update_doc)
356
- return True
357
- except Exception as e:
358
- logger.error(f"Error touching canonical file {public_hash}: {e}", exc_info=True)
359
- if raise_on_error:
360
- raise
361
- return False
362
-
363
- async def update_file_id(
364
- self,
365
- public_hash: str,
366
- file_id: str,
367
- *,
368
- raise_on_error: bool = False
369
- ) -> bool:
370
- try:
371
- await self.files_col.update_one(
372
- {"public_hash": public_hash},
373
- {
374
- "$set": {
375
- "file_id": file_id,
376
- "last_seen_at": datetime.datetime.now(datetime.timezone.utc)
377
- }
378
- }
379
- )
380
- return True
381
- except Exception as e:
382
- logger.error(f"Error updating file_id for {public_hash}: {e}", exc_info=True)
383
- if raise_on_error:
384
- raise
385
- return False
386
-
387
- async def acquire_file_ingest_claim(
388
- self,
389
- file_unique_id: str,
390
- *,
391
- ttl_seconds: int = 60
392
- ) -> bool:
393
- now = datetime.datetime.now(datetime.timezone.utc)
394
- claim_fields = {
395
- "created_at": now,
396
- "expires_at": now + datetime.timedelta(seconds=ttl_seconds)
397
- }
398
- try:
399
- await self.file_ingest_locks_col.insert_one({
400
- "_id": file_unique_id,
401
- **claim_fields
402
- })
403
- return True
404
- except DuplicateKeyError:
405
- try:
406
- result = await self.file_ingest_locks_col.find_one_and_update(
407
- {
408
- "_id": file_unique_id,
409
- "$or": [
410
- {"expires_at": {"$lte": now}},
411
- {"expires_at": {"$exists": False}}
412
- ]
413
- },
414
- {
415
- "$set": claim_fields
416
- },
417
- return_document=False
418
- )
419
- return bool(result)
420
- except Exception as e:
421
- logger.error(f"Error updating ingest claim for {file_unique_id}: {e}", exc_info=True)
422
- raise
423
- except Exception as e:
424
- logger.error(f"Error acquiring ingest claim for {file_unique_id}: {e}", exc_info=True)
425
- raise
426
-
427
- async def release_file_ingest_claim(self, file_unique_id: str) -> bool:
428
- try:
429
- await self.file_ingest_locks_col.delete_one({"_id": file_unique_id})
430
- return True
431
- except Exception as e:
432
- logger.error(f"Error releasing ingest claim for {file_unique_id}: {e}", exc_info=True)
433
- return False
434
-
435
- async def is_file_ingest_claim_active(self, file_unique_id: str) -> bool:
436
- try:
437
- claim = await self.file_ingest_locks_col.find_one(
438
- {
439
- "_id": file_unique_id,
440
- "expires_at": {"$gt": datetime.datetime.now(datetime.timezone.utc)}
441
- },
442
- {"_id": 1}
443
- )
444
- return bool(claim)
445
- except Exception as e:
446
- logger.error(f"Error checking ingest claim for {file_unique_id}: {e}", exc_info=True)
447
- raise
448
-
449
- async def close(self):
450
- if self._client:
451
- await self._client.close()
452
-
453
- db = Database(Var.DATABASE_URL, Var.NAME)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import secrets
2
+ # Thunder/utils/database.py
3
+
4
+ import datetime
5
+ from typing import Any, Dict, Optional
6
+ from pymongo import AsyncMongoClient
7
+ from pymongo.asynchronous.collection import AsyncCollection
8
+ from pymongo.errors import DuplicateKeyError
9
+ from Thunder.vars import Var
10
+ from Thunder.utils.logger import logger
11
+
12
+ class Database:
13
+ def __init__(self, uri: str, database_name: str, *args, **kwargs):
14
+ self._client = AsyncMongoClient(uri, *args, **kwargs)
15
+ self.db = self._client[database_name]
16
+ self.col: AsyncCollection = self.db.users
17
+ self.banned_users_col: AsyncCollection = self.db.banned_users
18
+ self.banned_channels_col: AsyncCollection = self.db.banned_channels
19
+ self.token_col: AsyncCollection = self.db.tokens
20
+ self.authorized_users_col: AsyncCollection = self.db.authorized_users
21
+ self.restart_message_col: AsyncCollection = self.db.restart_message
22
+ self.files_col: AsyncCollection = self.db.files
23
+ self.file_ingest_locks_col: AsyncCollection = self.db.file_ingest_locks
24
+ self.subscriptions_col: AsyncCollection = self.db.subscriptions
25
+ self.temp_passes_col: AsyncCollection = self.db.temp_passes
26
+ self.sub_requests_col: AsyncCollection = self.db.subscription_requests
27
+
28
+ async def _deduplicate_users(self) -> None:
29
+ pipeline = [
30
+ {"$sort": {"join_date": 1}},
31
+ {"$group": {"_id": "$id", "doc_id": {"$first": "$_id"}}},
32
+ {"$project": {"_id": "$doc_id"}}
33
+ ]
34
+ keep_ids = []
35
+ async for doc in self.col.aggregate(pipeline):
36
+ keep_ids.append(doc["_id"])
37
+ if keep_ids:
38
+ result = await self.col.delete_many({"_id": {"$nin": keep_ids}})
39
+ if result.deleted_count > 0:
40
+ logger.warning(f"Deduplicated {result.deleted_count} duplicate user documents.")
41
+
42
+ async def ensure_indexes(self, *, raise_on_error: bool = True) -> bool:
43
+ try:
44
+ await self.banned_users_col.create_index("user_id", unique=True)
45
+ await self.banned_channels_col.create_index("channel_id", unique=True)
46
+ await self.token_col.create_index("token", unique=True)
47
+ await self.authorized_users_col.create_index("user_id", unique=True)
48
+ try:
49
+ await self.col.create_index("id", unique=True)
50
+ except DuplicateKeyError:
51
+ logger.warning("Duplicate users found, deduplicating...")
52
+ await self._deduplicate_users()
53
+ await self.col.create_index("id", unique=True)
54
+ await self.token_col.create_index("expires_at", expireAfterSeconds=0)
55
+ await self.token_col.create_index("activated")
56
+ await self.restart_message_col.create_index("message_id", unique=True)
57
+ await self.restart_message_col.create_index("timestamp", expireAfterSeconds=3600)
58
+ await self.files_col.create_index("file_unique_id", unique=True)
59
+ await self.files_col.create_index("public_hash", unique=True)
60
+ await self.files_col.create_index("canonical_message_id", unique=True)
61
+ await self.files_col.create_index("created_at")
62
+ await self.files_col.create_index("last_seen_at")
63
+ await self.file_ingest_locks_col.create_index("expires_at", expireAfterSeconds=0)
64
+ await self.subscriptions_col.create_index("user_id", unique=True)
65
+ await self.subscriptions_col.create_index("expires_at")
66
+ await self.subscriptions_col.create_index("status")
67
+ await self.temp_passes_col.create_index([("user_id", 1), ("file_id", 1)], unique=True)
68
+ await self.temp_passes_col.create_index("expires_at", expireAfterSeconds=0)
69
+ await self.sub_requests_col.create_index("request_id", unique=True)
70
+ await self.sub_requests_col.create_index("user_id")
71
+
72
+ logger.debug("Database indexes ensured.")
73
+ return True
74
+ except Exception as e:
75
+ logger.error(f"Error in ensure_indexes: {e}", exc_info=True)
76
+ if raise_on_error:
77
+ raise
78
+ return False
79
+
80
+ def new_user(self, user_id: int) -> dict:
81
+ try:
82
+ return {
83
+ 'id': user_id,
84
+ 'join_date': datetime.datetime.now(datetime.timezone.utc)
85
+ }
86
+ except Exception as e:
87
+ logger.error(f"Error in new_user for user {user_id}: {e}", exc_info=True)
88
+ raise
89
+
90
+ async def add_user(self, user_id: int) -> bool:
91
+ try:
92
+ result = await self.col.update_one(
93
+ {'id': user_id},
94
+ {'$setOnInsert': self.new_user(user_id)},
95
+ upsert=True
96
+ )
97
+ if result.upserted_id:
98
+ logger.debug(f"Added new user {user_id} to database.")
99
+ return True
100
+ return False
101
+ except Exception as e:
102
+ logger.error(f"Error in add_user for user {user_id}: {e}", exc_info=True)
103
+ raise
104
+
105
+
106
+ async def is_user_exist(self, user_id: int) -> bool:
107
+ """Read-only existence check. For user registration, use add_user() instead."""
108
+ try:
109
+ user = await self.col.find_one({'id': user_id}, {'_id': 1})
110
+ return bool(user)
111
+ except Exception as e:
112
+ logger.error(f"Error in is_user_exist for user {user_id}: {e}", exc_info=True)
113
+ raise
114
+
115
+ async def total_users_count(self) -> int:
116
+ try:
117
+ return await self.col.count_documents({})
118
+ except Exception as e:
119
+ logger.error(f"Error in total_users_count: {e}", exc_info=True)
120
+ return 0
121
+
122
+ async def get_authorized_users_count(self) -> int:
123
+ try:
124
+ return await self.authorized_users_col.count_documents({})
125
+ except Exception as e:
126
+ logger.error(f"Error in get_authorized_users_count: {e}", exc_info=True)
127
+ return 0
128
+
129
+ async def get_regular_users_count(self) -> int:
130
+ try:
131
+ auth_ids = await self.authorized_users_col.distinct("user_id")
132
+ return await self.col.count_documents({"id": {"$nin": auth_ids}})
133
+ except Exception as e:
134
+ logger.error(f"Error in get_regular_users_count: {e}", exc_info=True)
135
+ return 0
136
+
137
+ async def get_all_users(self):
138
+ try:
139
+ return self.col.find({})
140
+ except Exception as e:
141
+ logger.error(f"Error in get_all_users: {e}", exc_info=True)
142
+ return self.col.find({"_id": {"$exists": False}})
143
+
144
+ async def get_authorized_users_cursor(self):
145
+ try:
146
+ return self.authorized_users_col.find({})
147
+ except Exception as e:
148
+ logger.error(f"Error in get_authorized_users_cursor: {e}", exc_info=True)
149
+ return self.authorized_users_col.find({"_id": {"$exists": False}})
150
+
151
+ async def get_regular_users_cursor(self):
152
+ try:
153
+ auth_ids = await self.authorized_users_col.distinct("user_id")
154
+ return self.col.find({"id": {"$nin": auth_ids}})
155
+ except Exception as e:
156
+ logger.error(f"Error in get_regular_users_cursor: {e}", exc_info=True)
157
+ return self.col.find({"_id": {"$exists": False}})
158
+
159
+ async def delete_user(self, user_id: int):
160
+ try:
161
+ await self.col.delete_one({'id': user_id})
162
+ logger.debug(f"Deleted user {user_id}.")
163
+ except Exception as e:
164
+ logger.error(f"Error in delete_user for user {user_id}: {e}", exc_info=True)
165
+ raise
166
+
167
+
168
+ async def add_banned_user(
169
+ self, user_id: int, banned_by: Optional[int] = None,
170
+ reason: Optional[str] = None
171
+ ):
172
+ try:
173
+ ban_data = {
174
+ "user_id": user_id,
175
+ "banned_at": datetime.datetime.now(datetime.timezone.utc),
176
+ "banned_by": banned_by,
177
+ "reason": reason
178
+ }
179
+ await self.banned_users_col.update_one(
180
+ {"user_id": user_id},
181
+ {"$set": ban_data},
182
+ upsert=True
183
+ )
184
+ logger.debug(f"Added/Updated banned user {user_id}. Reason: {reason}")
185
+ except Exception as e:
186
+ logger.error(f"Error in add_banned_user for user {user_id}: {e}", exc_info=True)
187
+ raise
188
+
189
+ async def remove_banned_user(self, user_id: int) -> bool:
190
+ try:
191
+ result = await self.banned_users_col.delete_one({"user_id": user_id})
192
+ if result.deleted_count > 0:
193
+ logger.debug(f"Removed banned user {user_id}.")
194
+ return True
195
+ return False
196
+ except Exception as e:
197
+ logger.error(f"Error in remove_banned_user for user {user_id}: {e}", exc_info=True)
198
+ return False
199
+
200
+ async def is_user_banned(self, user_id: int) -> Optional[Dict[str, Any]]:
201
+ try:
202
+ return await self.banned_users_col.find_one({"user_id": user_id})
203
+ except Exception as e:
204
+ logger.error(f"Error in is_user_banned for user {user_id}: {e}", exc_info=True)
205
+ return None
206
+
207
+ async def add_banned_channel(
208
+ self, channel_id: int, banned_by: Optional[int] = None,
209
+ reason: Optional[str] = None
210
+ ):
211
+ try:
212
+ ban_data = {
213
+ "channel_id": channel_id,
214
+ "banned_at": datetime.datetime.now(datetime.timezone.utc),
215
+ "banned_by": banned_by,
216
+ "reason": reason
217
+ }
218
+ await self.banned_channels_col.update_one(
219
+ {"channel_id": channel_id},
220
+ {"$set": ban_data},
221
+ upsert=True
222
+ )
223
+ logger.debug(f"Added/Updated banned channel {channel_id}. Reason: {reason}")
224
+ except Exception as e:
225
+ logger.error(f"Error in add_banned_channel for channel {channel_id}: {e}", exc_info=True)
226
+ raise
227
+
228
+ async def remove_banned_channel(self, channel_id: int) -> bool:
229
+ try:
230
+ result = await self.banned_channels_col.delete_one({"channel_id": channel_id})
231
+ if result.deleted_count > 0:
232
+ logger.debug(f"Removed banned channel {channel_id}.")
233
+ return True
234
+ return False
235
+ except Exception as e:
236
+ logger.error(f"Error in remove_banned_channel for channel {channel_id}: {e}", exc_info=True)
237
+ return False
238
+
239
+ async def is_channel_banned(self, channel_id: int) -> Optional[Dict[str, Any]]:
240
+ try:
241
+ return await self.banned_channels_col.find_one({"channel_id": channel_id})
242
+ except Exception as e:
243
+ logger.error(f"Error in is_channel_banned for channel {channel_id}: {e}", exc_info=True)
244
+ return None
245
+
246
+ async def save_main_token(self, user_id: int, token_value: str, expires_at: datetime.datetime, created_at: datetime.datetime, activated: bool) -> None:
247
+ try:
248
+ await self.token_col.update_one(
249
+ {"user_id": user_id, "token": token_value},
250
+ {"$set": {
251
+ "expires_at": expires_at,
252
+ "created_at": created_at,
253
+ "activated": activated
254
+ }
255
+ },
256
+ upsert=True
257
+ )
258
+ logger.debug(f"Saved main token {token_value} for user {user_id} with activated status {activated}.")
259
+ except Exception as e:
260
+ logger.error(f"Error saving main token for user {user_id}: {e}", exc_info=True)
261
+ raise
262
+
263
+
264
+ async def add_restart_message(self, message_id: int, chat_id: int) -> None:
265
+ try:
266
+ await self.restart_message_col.insert_one({
267
+ "message_id": message_id,
268
+ "chat_id": chat_id,
269
+ "timestamp": datetime.datetime.now(datetime.timezone.utc)
270
+ })
271
+ logger.debug(f"Added restart message {message_id} for chat {chat_id}.")
272
+ except Exception as e:
273
+ logger.error(f"Error adding restart message {message_id}: {e}", exc_info=True)
274
+
275
+ async def get_restart_message(self) -> Optional[Dict[str, Any]]:
276
+ try:
277
+ return await self.restart_message_col.find_one(sort=[("timestamp", -1)])
278
+ except Exception as e:
279
+ logger.error(f"Error getting restart message: {e}", exc_info=True)
280
+ return None
281
+
282
+ async def delete_restart_message(self, message_id: int) -> None:
283
+ try:
284
+ await self.restart_message_col.delete_one({"message_id": message_id})
285
+ logger.debug(f"Deleted restart message {message_id}.")
286
+ except Exception as e:
287
+ logger.error(f"Error deleting restart message {message_id}: {e}", exc_info=True)
288
+
289
+ async def is_user_authorized(self, user_id: int) -> bool:
290
+ try:
291
+ user = await self.authorized_users_col.find_one({'user_id': user_id}, {'_id': 1})
292
+ return bool(user)
293
+ except Exception as e:
294
+ logger.error(f"Error in is_user_authorized for user {user_id}: {e}", exc_info=True)
295
+ return False
296
+
297
+ async def get_file_by_unique_id(self, file_unique_id: str) -> Optional[Dict[str, Any]]:
298
+ try:
299
+ return await self.files_col.find_one({"file_unique_id": file_unique_id})
300
+ except Exception as e:
301
+ logger.error(f"Error getting file by unique_id {file_unique_id}: {e}", exc_info=True)
302
+ return None
303
+
304
+ async def get_file_by_hash(
305
+ self,
306
+ public_hash: str,
307
+ *,
308
+ raise_on_error: bool = True
309
+ ) -> Optional[Dict[str, Any]]:
310
+ try:
311
+ return await self.files_col.find_one({"public_hash": public_hash})
312
+ except Exception as e:
313
+ logger.error(f"Error getting file by hash {public_hash}: {e}", exc_info=True)
314
+ if raise_on_error:
315
+ raise
316
+ return None
317
+
318
+ async def get_file_by_message_id(self, canonical_message_id: int) -> Optional[Dict[str, Any]]:
319
+ try:
320
+ return await self.files_col.find_one({"canonical_message_id": canonical_message_id})
321
+ except Exception as e:
322
+ logger.error(
323
+ f"Error getting file by message_id {canonical_message_id}: {e}",
324
+ exc_info=True
325
+ )
326
+ return None
327
+
328
+ async def create_file_record(self, file_record: Dict[str, Any]) -> None:
329
+ try:
330
+ await self.files_col.insert_one(file_record)
331
+ except Exception as e:
332
+ logger.error(
333
+ f"Error creating canonical file record for {file_record.get('file_unique_id')}: {e}",
334
+ exc_info=True
335
+ )
336
+ raise
337
+
338
+ async def replace_file_record(self, file_record: Dict[str, Any]) -> None:
339
+ try:
340
+ await self.files_col.replace_one(
341
+ {"file_unique_id": file_record["file_unique_id"]},
342
+ file_record,
343
+ upsert=True
344
+ )
345
+ except Exception as e:
346
+ logger.error(
347
+ f"Error replacing canonical file record for {file_record.get('file_unique_id')}: {e}",
348
+ exc_info=True
349
+ )
350
+ raise
351
+
352
+ async def touch_file_record(
353
+ self,
354
+ public_hash: str,
355
+ *,
356
+ reused: bool = False,
357
+ raise_on_error: bool = False
358
+ ) -> bool:
359
+ try:
360
+ update_doc: Dict[str, Any] = {
361
+ "$set": {"last_seen_at": datetime.datetime.now(datetime.timezone.utc)},
362
+ "$inc": {"seen_count": 1}
363
+ }
364
+ if reused:
365
+ update_doc["$inc"]["reuse_count"] = 1
366
+ await self.files_col.update_one({"public_hash": public_hash}, update_doc)
367
+ return True
368
+ except Exception as e:
369
+ logger.error(f"Error touching canonical file {public_hash}: {e}", exc_info=True)
370
+ if raise_on_error:
371
+ raise
372
+ return False
373
+
374
+ async def update_file_id(
375
+ self,
376
+ public_hash: str,
377
+ file_id: str,
378
+ *,
379
+ raise_on_error: bool = False
380
+ ) -> bool:
381
+ try:
382
+ await self.files_col.update_one(
383
+ {"public_hash": public_hash},
384
+ {
385
+ "$set": {
386
+ "file_id": file_id,
387
+ "last_seen_at": datetime.datetime.now(datetime.timezone.utc)
388
+ }
389
+ }
390
+ )
391
+ return True
392
+ except Exception as e:
393
+ logger.error(f"Error updating file_id for {public_hash}: {e}", exc_info=True)
394
+ if raise_on_error:
395
+ raise
396
+ return False
397
+
398
+ async def acquire_file_ingest_claim(
399
+ self,
400
+ file_unique_id: str,
401
+ *,
402
+ ttl_seconds: int = 60
403
+ ) -> bool:
404
+ now = datetime.datetime.now(datetime.timezone.utc)
405
+ claim_fields = {
406
+ "created_at": now,
407
+ "expires_at": now + datetime.timedelta(seconds=ttl_seconds)
408
+ }
409
+ try:
410
+ await self.file_ingest_locks_col.insert_one({
411
+ "_id": file_unique_id,
412
+ **claim_fields
413
+ })
414
+ return True
415
+ except DuplicateKeyError:
416
+ try:
417
+ result = await self.file_ingest_locks_col.find_one_and_update(
418
+ {
419
+ "_id": file_unique_id,
420
+ "$or": [
421
+ {"expires_at": {"$lte": now}},
422
+ {"expires_at": {"$exists": False}}
423
+ ]
424
+ },
425
+ {
426
+ "$set": claim_fields
427
+ },
428
+ return_document=False
429
+ )
430
+ return bool(result)
431
+ except Exception as e:
432
+ logger.error(f"Error updating ingest claim for {file_unique_id}: {e}", exc_info=True)
433
+ raise
434
+ except Exception as e:
435
+ logger.error(f"Error acquiring ingest claim for {file_unique_id}: {e}", exc_info=True)
436
+ raise
437
+
438
+ async def release_file_ingest_claim(self, file_unique_id: str) -> bool:
439
+ try:
440
+ await self.file_ingest_locks_col.delete_one({"_id": file_unique_id})
441
+ return True
442
+ except Exception as e:
443
+ logger.error(f"Error releasing ingest claim for {file_unique_id}: {e}", exc_info=True)
444
+ return False
445
+
446
+ async def is_file_ingest_claim_active(self, file_unique_id: str) -> bool:
447
+ try:
448
+ claim = await self.file_ingest_locks_col.find_one(
449
+ {
450
+ "_id": file_unique_id,
451
+ "expires_at": {"$gt": datetime.datetime.now(datetime.timezone.utc)}
452
+ },
453
+ {"_id": 1}
454
+ )
455
+ return bool(claim)
456
+ except Exception as e:
457
+ logger.error(f"Error checking ingest claim for {file_unique_id}: {e}", exc_info=True)
458
+ raise
459
+
460
+
461
+ async def is_subscribed(self, user_id: int) -> bool:
462
+ try:
463
+ now = datetime.datetime.now(datetime.timezone.utc)
464
+ doc = await self.subscriptions_col.find_one({
465
+ "user_id": user_id,
466
+ "status": "active",
467
+ "expires_at": {"$gt": now}
468
+ })
469
+ return bool(doc)
470
+ except Exception as e:
471
+ logger.error(f"Error checking subscription for user {user_id}: {e}", exc_info=True)
472
+ return False
473
+
474
+ async def get_subscription(self, user_id: int) -> Optional[Dict[str, Any]]:
475
+ try:
476
+ return await self.subscriptions_col.find_one({"user_id": user_id})
477
+ except Exception as e:
478
+ logger.error(f"Error getting subscription for user {user_id}: {e}", exc_info=True)
479
+ return None
480
+
481
+ async def add_subscription(self, user_id: int, days: int = 30, tx_ref: str = "", plan: str = "monthly") -> Dict[str, Any]:
482
+ try:
483
+ now = datetime.datetime.now(datetime.timezone.utc)
484
+ existing = await self.get_subscription(user_id)
485
+ if existing and existing.get("status") == "active" and existing.get("expires_at") and existing["expires_at"] > now:
486
+ new_expiry = existing["expires_at"] + datetime.timedelta(days=days)
487
+ else:
488
+ new_expiry = now + datetime.timedelta(days=days)
489
+
490
+ doc = {
491
+ "user_id": user_id,
492
+ "plan": plan,
493
+ "status": "active",
494
+ "activated_at": now,
495
+ "expires_at": new_expiry,
496
+ "tx_ref": tx_ref or (existing.get("tx_ref", "") if existing else "")
497
+ }
498
+ await self.subscriptions_col.update_one(
499
+ {"user_id": user_id},
500
+ {"$set": doc},
501
+ upsert=True
502
+ )
503
+ logger.info(f"Subscription added for user {user_id}: valid until {new_expiry.isoformat()}")
504
+ return doc
505
+ except Exception as e:
506
+ logger.error(f"Error adding subscription for user {user_id}: {e}", exc_info=True)
507
+ raise
508
+
509
+ async def remove_subscription(self, user_id: int) -> bool:
510
+ try:
511
+ res = await self.subscriptions_col.update_one(
512
+ {"user_id": user_id},
513
+ {"$set": {"status": "revoked", "revoked_at": datetime.datetime.now(datetime.timezone.utc)}}
514
+ )
515
+ return res.modified_count > 0
516
+ except Exception as e:
517
+ logger.error(f"Error removing subscription for user {user_id}: {e}", exc_info=True)
518
+ return False
519
+
520
+ async def get_active_subscribers_count(self) -> int:
521
+ try:
522
+ now = datetime.datetime.now(datetime.timezone.utc)
523
+ return await self.subscriptions_col.count_documents({
524
+ "status": "active",
525
+ "expires_at": {"$gt": now}
526
+ })
527
+ except Exception as e:
528
+ logger.error(f"Error counting subscribers: {e}", exc_info=True)
529
+ return 0
530
+
531
+ async def get_all_subscribers(self, limit: int = 50) -> list:
532
+ try:
533
+ now = datetime.datetime.now(datetime.timezone.utc)
534
+ cursor = self.subscriptions_col.find({
535
+ "status": "active",
536
+ "expires_at": {"$gt": now}
537
+ }).sort("expires_at", -1).limit(limit)
538
+ return [doc async for doc in cursor]
539
+ except Exception as e:
540
+ logger.error(f"Error listing subscribers: {e}", exc_info=True)
541
+ return []
542
+
543
+ async def add_temp_pass(self, user_id: int, file_id: str, hours: int = 24) -> bool:
544
+ try:
545
+ now = datetime.datetime.now(datetime.timezone.utc)
546
+ expiry = now + datetime.timedelta(hours=hours)
547
+ await self.temp_passes_col.update_one(
548
+ {"user_id": user_id, "file_id": str(file_id)},
549
+ {"$set": {"created_at": now, "expires_at": expiry, "used": False}},
550
+ upsert=True
551
+ )
552
+ logger.info(f"Temporary pass added for user {user_id}, file {file_id} (valid {hours}h)")
553
+ return True
554
+ except Exception as e:
555
+ logger.error(f"Error adding temp pass for {user_id}: {e}", exc_info=True)
556
+ return False
557
+
558
+ async def has_temp_pass(self, user_id: int, file_id: str) -> bool:
559
+ try:
560
+ now = datetime.datetime.now(datetime.timezone.utc)
561
+ doc = await self.temp_passes_col.find_one({
562
+ "user_id": user_id,
563
+ "file_id": str(file_id),
564
+ "expires_at": {"$gt": now}
565
+ })
566
+ return bool(doc)
567
+ except Exception as e:
568
+ logger.error(f"Error checking temp pass for {user_id}: {e}", exc_info=True)
569
+ return False
570
+
571
+ async def create_sub_request(
572
+ self,
573
+ user_id: int,
574
+ user_name: str,
575
+ req_type: str,
576
+ file_id: Optional[str] = None,
577
+ tx_ref: Optional[str] = None,
578
+ proof_file_id: Optional[str] = None
579
+ ) -> str:
580
+ try:
581
+ req_id = secrets.token_hex(6)
582
+ doc = {
583
+ "request_id": req_id,
584
+ "user_id": user_id,
585
+ "user_name": user_name,
586
+ "req_type": req_type,
587
+ "file_id": str(file_id) if file_id else None,
588
+ "tx_ref": tx_ref,
589
+ "proof_file_id": proof_file_id,
590
+ "status": "pending",
591
+ "created_at": datetime.datetime.now(datetime.timezone.utc)
592
+ }
593
+ await self.sub_requests_col.insert_one(doc)
594
+ return req_id
595
+ except Exception as e:
596
+ logger.error(f"Error creating subscription request: {e}", exc_info=True)
597
+ raise
598
+
599
+ async def get_sub_request(self, request_id: str) -> Optional[Dict[str, Any]]:
600
+ try:
601
+ return await self.sub_requests_col.find_one({"request_id": request_id})
602
+ except Exception as e:
603
+ logger.error(f"Error fetching subscription request {request_id}: {e}", exc_info=True)
604
+ return None
605
+
606
+ async def update_sub_request_status(self, request_id: str, status: str, admin_id: int) -> bool:
607
+ try:
608
+ res = await self.sub_requests_col.update_one(
609
+ {"request_id": request_id},
610
+ {"$set": {
611
+ "status": status,
612
+ "decided_by": admin_id,
613
+ "decided_at": datetime.datetime.now(datetime.timezone.utc)
614
+ }}
615
+ )
616
+ return res.modified_count > 0
617
+ except Exception as e:
618
+ logger.error(f"Error updating request {request_id}: {e}", exc_info=True)
619
+ return False
620
+
621
+ async def close(self):
622
+ if self._client:
623
+ await self._client.close()
624
+
625
+ db = Database(Var.DATABASE_URL, Var.NAME)
Thunder/utils/messages.py CHANGED
@@ -1,399 +1,454 @@
1
- # Thunder/utils/messages.py
2
-
3
- # =====================================================================================
4
- # ====== ERROR MESSAGES ======
5
- # =====================================================================================
6
-
7
- # ------ General Errors ------
8
- MSG_ERROR_GENERIC = "⚠️ **Oops!** Something went wrong. Please try again. If the issue persists, contact support."
9
- MSG_ERROR_USER_INFO = "❗ **User Not Found:** Couldn't find user. Please check the ID or Username."
10
-
11
- # ------ User Input & Validation Errors ------
12
- MSG_INVALID_USER_ID = "❌ **Invalid User ID:** Please provide a numeric user ID."
13
- MSG_ERROR_START_BOT = "⚠️ You need to start the bot in private first to use this command.\n👉 [Click here]({invite_link}) to start a private chat."
14
- MSG_ERROR_REPLY_FILE = "⚠️ Please use the /link command in reply to a file."
15
- MSG_ERROR_NO_FILE = "⚠️ The message you're replying to does not contain any file."
16
- MSG_ERROR_INVALID_NUMBER = "⚠️ **Invalid number specified.**"
17
- MSG_ERROR_NUMBER_RANGE = "⚠️ **Please specify a number between 1 and {max_files}.**"
18
- MSG_ERROR_DM_FAILED = "⚠️ I couldn't send you a Direct Message. Please start the bot first."
19
-
20
- # ------ File & Media Errors ------
21
- MSG_ERROR_PROCESSING_MEDIA = "⚠️ **Oops!** Something went wrong while processing your media. Please try again. If the issue persists, contact support."
22
-
23
- # ------ Admin Action Errors (Ban, Auth, etc.) ------
24
- MSG_AUTHORIZE_FAILED = (
25
- "❌ **Authorization Failed:** "
26
- "Could not authorize user `{user_id}`."
27
- )
28
- MSG_DEAUTHORIZE_FAILED = (
29
- "❌ **Deauthorization Failed:** "
30
- "User `{user_id}` was not authorized or an error occurred."
31
- )
32
- MSG_TOKEN_FAILED = (
33
- "⚠️ **Token Activation Failed!**\n\n"
34
- "> ❗ Reason: {reason}\n\n"
35
- "🔑 Please check your token or contact support."
36
- )
37
- MSG_SHELL_ERROR = """**❌ Shell Command Error ❌**
38
- <pre>{error}</pre>"""
39
-
40
- # ------ System & Bot Errors ------
41
- MSG_ERROR_NOT_ADMIN = "⚠️ **Admin Required:** I need admin privileges to work here."
42
- MSG_DC_INVALID_USAGE = "🤔 **Invalid Usage:** Please reply to a user's message or a media file to get DC info."
43
- MSG_DC_ANON_ERROR = "😥 **Cannot Get Your DC Info:** Unable to identify you. This command might not work for anonymous users."
44
- MSG_DC_FILE_ERROR = "⚙️ **Error Getting File DC Info:** Could not fetch details. File might be inaccessible."
45
- MSG_STATS_ERROR = "❌ **Stats Error:** Could not retrieve system statistics."
46
- MSG_STATUS_ERROR = "❌ **Status Error:** Could not retrieve system status."
47
- MSG_DB_ERROR = "❌ **Database Error:** Could not retrieve user count."
48
- MSG_CRITICAL_ERROR = (
49
- "🚨 **Critical Media Processing Error** 🚨\n\n"
50
- "> ⚠️ Details:\n```\n{error}\n```\n\n"
51
- "Please investigate immediately! (ID: {error_id})"
52
- )
53
-
54
- # =====================================================================================
55
- # ====== ADMIN MESSAGES ======
56
- # =====================================================================================
57
-
58
- # ------ Ban/Unban ------
59
- MSG_DECORATOR_BANNED = "You are currently banned and cannot use this bot.\nReason: {reason}\nBanned on: {ban_time}"
60
- MSG_BAN_USAGE = "⚠️ **Usage:** /ban [user_id] [reason]"
61
- MSG_CANNOT_BAN_OWNER = "❌ **Cannot ban an owner.**"
62
- MSG_ADMIN_USER_BANNED = "✅ **User {user_id} has been banned."
63
- MSG_BAN_REASON_SUFFIX = "\n📝 **Reason:** {reason}"
64
- MSG_ADMIN_NO_BAN_REASON = "No reason provided"
65
- MSG_USER_BANNED_NOTIFICATION = "🚫 **You have been banned from using this bot.**"
66
- MSG_UNBAN_USAGE = "⚠️ **Usage:** /unban <user_id>"
67
- MSG_ADMIN_USER_UNBANNED = "✅ **User {user_id} has been unbanned."
68
- MSG_USER_UNBANNED_NOTIFICATION = "🎉 **You have been unbanned from using this bot.**"
69
- MSG_USER_NOT_IN_BAN_LIST = "ℹ️ **User {user_id} was not found in the ban list."
70
- MSG_CHANNEL_BANNED = "✅ **Channel {channel_id} has been banned.**"
71
- MSG_CHANNEL_BANNED_REASON_SUFFIX = "\n📝 **Reason:** {reason}"
72
- MSG_CHANNEL_UNBANNED = "✅ **Channel {channel_id} has been unbanned.**"
73
- MSG_CHANNEL_NOT_BANNED = "ℹ️ **Channel {channel_id} was not found in the ban list.**"
74
-
75
- # ------ Token & Authorization ------
76
- MSG_AUTHORIZE_USAGE = "🔑 **Usage:** `/authorize <user_id>`"
77
- MSG_DEAUTHORIZE_USAGE = "🔒 **Usage:** `/deauthorize <user_id>`"
78
- MSG_AUTHORIZE_SUCCESS = (
79
- "✅ **User Authorized!**\n\n"
80
- "> 👤 User ID: `{user_id}`\n"
81
- "> 🔑 Access: Permanent"
82
- )
83
- MSG_DEAUTHORIZE_SUCCESS = (
84
- "✅ **User Deauthorized!**\n\n"
85
- "> 👤 User ID: `{user_id}`\n"
86
- "> 🔒 Access: Revoked"
87
- )
88
- MSG_TOKEN_ACTIVATED = "✅ Token successfully activated!\n\n⏳ This token is valid for {duration_hours} hours."
89
- MSG_TOKEN_INVALID = "🚫 **Expired or Invalid Token.** Please click the button below to activate your access token."
90
- MSG_NO_AUTH_USERS = "ℹ️ **No Authorized Users Found:** The list is currently empty."
91
  MSG_AUTH_USER_INFO = """{i}. 👤: {display_name}
92
  • User ID: `{user_id}`
93
  • Authorized by: `{authorized_by}`
94
  • Date: `{auth_time}`\n\n"""
95
- MSG_ADMIN_AUTH_LIST_HEADER = "🔐 **Authorized Users List**\n\n"
96
-
97
- # ------ Shell Commands ------
98
- MSG_SHELL_USAGE = (
99
- "<b>Usage:</b>\n"
100
- "/shell <command>\n\n"
101
- "<b>Example:</b>\n"
102
- "/shell ls -l"
103
- )
104
- MSG_SHELL_EXECUTING = "Executing Command... ⚙️\n<pre>{command}</pre>"
105
- MSG_SHELL_OUTPUT = """**Shell Command Output:**
106
- <pre>{output}</pre>"""
107
- MSG_SHELL_OUTPUT_STDOUT = "<b>[stdout]:</b>\n<pre>{output}</pre>"
108
- MSG_SHELL_OUTPUT_STDERR = "<b>[stderr]:</b>\n<pre>{error}</pre>"
109
- MSG_SHELL_NO_OUTPUT = "✅ <b>Command Executed:</b> No output."
110
-
111
- # ------ Admin View & Control ------
112
-
113
- MSG_WORKLOAD_ITEM = " {bot_name}: {load}\n"
114
- MSG_ADMIN_RESTART_DONE = "✅ **Restart Successful!**"
115
- MSG_RESTARTING = "♻️ **Updating and Restarting Bot...**\n\n> ⏳ Please wait a moment."
116
- MSG_LOG_FILE_CAPTION = "📄 **System Logs**"
117
-
118
- MSG_LOG_FILE_EMPTY = "ℹ️ **Log File Empty:** No data found in the log file."
119
- MSG_LOG_FILE_MISSING = "⚠️ **Log File Missing:** Could not find the log file."
120
-
121
- # =====================================================================================
122
- # ====== BUTTON TEXTS (User-facing) ======
123
- # =====================================================================================
124
-
125
- MSG_BUTTON_STREAM_NOW = "🖥️ Stream"
126
- MSG_BUTTON_DOWNLOAD = "🚀 Download"
127
- MSG_BUTTON_GET_HELP = "📖 Get Help"
128
- MSG_BUTTON_CANCEL_BROADCAST = "🛑 Cancel Broadcast"
129
- MSG_BUTTON_VIEW_PROFILE = "👤 View User Profile"
130
- MSG_BUTTON_ABOUT = "ℹ️ About Bot"
131
- MSG_BUTTON_JOIN_CHANNEL = "📢 Join {channel_title}"
132
- MSG_BUTTON_GITHUB = "🛠️ GitHub"
133
- MSG_BUTTON_START_CHAT = "📩 Start Chat"
134
- MSG_BUTTON_CLOSE = "✖ Close"
135
-
136
-
137
- # =====================================================================================
138
- # ====== COMMAND RESPONSES (User-facing) ======
139
- # =====================================================================================
140
-
141
- MSG_WELCOME = (
142
- "🌟 **Welcome, {user_name}!** 🌟\n\n"
143
- "I'm **Flash File** ⚡\n"
144
- "I generate direct download and streaming links for your files.\n\n"
145
- "**How to use:**\n"
146
- "1. Send any file to me for private links.\n"
147
- "2. In groups, reply to a file with `/link`.\n\n"
148
- "» Use `/help` for all commands and detailed information.\n\n"
149
- "🚀 Send a file to begin!"
150
- )
151
-
152
- MSG_HELP = (
153
- "📘 **Flash File - Help Guide** 📖\n\n"
154
- "How to get direct download & streaming links:\n\n"
155
- "**🚀 Private Chat (with me):**\n"
156
- "> 1. Send me **any file** (document, video, audio, photo, etc.).\n"
157
- "> 2. I'll instantly reply with your links! ⚡\n\n"
158
- "**👥 Using in Groups:**\n"
159
- "> • Reply to any file with `/link`.\n"
160
- "> • **Batch Mode:** Reply to the **first** file with `/link <number>` (e.g., `/link 5` for 5 files, up to {max_files}).\n"
161
- "> • Bot needs administrator rights in the group to function.\n"
162
- "> • Links are posted in the group & sent to you privately.\n\n"
163
- "**📢 Using in Channels:**\n"
164
- "> • Add me as an administrator with necessary permissions.\n"
165
- "> • I can be configured to auto-detect new media files.\n"
166
- "> • Inline stream/download buttons can be added to files automatically.\n"
167
- "> • Files from banned channels (owner configuration) are rejected.\n"
168
- "> • Auto-posting links if the bot has admin privileges with delete rights.\n\n"
169
- "**⚙️ Available Commands:**\n"
170
- "> `/start` 👋 - Welcome message & quick start information.\n"
171
- "> `/help` 📖 - Shows this help message.\n"
172
- "> `/link <num>` 🔗 - (Groups) Generate links. \n"
173
- "> `/about` ℹ️ - Learn more about me and my features.\n"
174
- "> `/ping` 📡 - Check my responsiveness and online status.\n"
175
- "> `/dc` 🌍 - View DC information (for yourself, another user, or a file).\n\n"
176
- "**💡 Pro Tips:**\n"
177
- "> • You can forward files from other chats directly to me.\n"
178
- "> • If you encounter a rate limit message, please wait the specified time. ⏳\n"
179
- "> • For `/link` in groups to work reliably (and for private link delivery), ensure you've started a private chat with me first.\n"
180
- "> • Processing batch files might take a bit longer. Please be patient. 🐌\n\n"
181
- "❓ Questions? Please ask in our support group!"
182
- )
183
-
184
- MSG_ABOUT = (
185
- "🌟 **About Flash File - File to Link Bot** ℹ️\n\n"
186
- "I'm your go-to bot for **instant download & streaming!** ⚡\n\n"
187
- "**🚀 Key Features:**\n"
188
- "> **Instant Links:** Get your links within seconds.\n"
189
- "> **Online Streaming:** Watch videos or listen to audio directly (for supported formats).\n"
190
- "> **Universal File Support:** Handles documents, videos, audio, photos, and more.\n"
191
- "> **High-Speed Access:** Optimized for fast link generation and file access.\n"
192
- "> **Secure & Reliable:** Your files are handled with care during processing.\n"
193
- "> **User-Friendly Interface:** Designed for ease of use on any device.\n"
194
- "> **Efficient Processing:** Built for speed and reliability.\n"
195
- "> **Batch Mode:** Process multiple files at once in groups using `/link <number>`.\n"
196
- "> **Versatile Usage:** Works in private chats, groups, and channels (with admin setup).\n\n"
197
- "💖 If you find me useful, please consider sharing me with your friends!"
198
- )
199
-
200
- # ------ Ping ------
201
- MSG_PING_START = "🛰️ **Pinging...** Please wait."
202
- MSG_PING_RESPONSE = (
203
- "☁️ **PONG! Bot is Online!** ⚡\n\n"
204
- "> ⏱️ **Ping:** {time_taken_ms:.2f} ms\n"
205
- "> 🤖 **Bot Status:** `Active`"
206
- )
207
-
208
- # ------ DC Info ------
209
- MSG_DC_USER_INFO = (
210
- "📍 **Information**\n"
211
- "> 👤 **User:** [{user_name}](tg://user?id={user_id})\n"
212
- "> 🆔 **User ID:** `{user_id}`\n"
213
- "> 🌍 **DC ID:** `{dc_id}`"
214
- )
215
-
216
- MSG_DC_FILE_INFO = (
217
- "🗂️ **File Information**\n"
218
- ">`{file_name}`\n"
219
- "💾 **File Size:** `{file_size}`\n"
220
- "📁 **File Type:** `{file_type}`\n"
221
- "🌍 **DC ID:** `{dc_id}`"
222
- )
223
-
224
- MSG_DC_UNKNOWN = "Unknown"
225
-
226
- # ------ File Link Generation ------
227
- MSG_DM_SINGLE_PREFIX = "📬 **From {chat_title}**\n"
228
- MSG_LINKS = (
229
- "✨ **Your Links are Ready!** ✨\n\n"
230
- "> `{file_name}`\n\n"
231
- "📂 **File Size:** `{file_size}`\n\n"
232
- "🚀 **Download Link:**\n`{download_link}`\n\n"
233
- "🖥️ **Stream Link:**\n`{stream_link}`\n\n"
234
- "⌛️ **Note: Links remain active while the bot is running and the file is accessible.**"
235
- )
236
-
237
- # =====================================================================================
238
- # ====== USER NOTIFICATIONS ======
239
- # =====================================================================================
240
-
241
- MSG_NEW_USER = (
242
- "✨ **New User Alert!** ✨\n"
243
- "> 👤 **Name:** [{first_name}](tg://user?id={user_id})\n"
244
- "> 🆔 **User ID:** `{user_id}`\n\n"
245
- )
246
- MSG_COMMUNITY_CHANNEL = "📢 **{channel_title}:** 🔒 Join this channel to use the bot."
247
-
248
- # =====================================================================================
249
- # ====== PROCESSING MESSAGES ======
250
- # =====================================================================================
251
-
252
- # ------ General File Processing ------
253
- MSG_PROCESSING_REQUEST = "⏳ **Processing your request...**"
254
- MSG_PROCESSING_FILE = "⏳ **Processing your file...**"
255
- MSG_NEW_FILE_REQUEST = (
256
- "> 👤 **Source:** [{source_info}](tg://user?id={id_})\n"
257
- "> 🆔 **ID:** `{id_}`\n\n"
258
- "🚀 **Download:** `{online_link}`\n\n"
259
- "🖥️ **Stream:** `{stream_link}`"
260
- )
261
-
262
- # ------ Batch Processing ------
263
- MSG_PROCESSING_BATCH = "♻️ **Processing Batch {batch_number}/{total_batches}** ({file_count} files)"
264
- MSG_PROCESSING_STATUS = "📊 **Processing Files:** {processed}/{total} complete, {failed} failed"
265
- MSG_BATCH_LINKS_READY = "🔗 Here are your {count} download links:"
266
- MSG_DM_BATCH_PREFIX = "📬 **Batch Links from {chat_title}**\n"
267
- MSG_PROCESSING_RESULT = "✅ **Process Complete:** {processed}/{total} files processed successfully, {failed} failed"
268
-
269
- # =====================================================================================
270
- # ====== BROADCAST MESSAGES ======
271
- # =====================================================================================
272
-
273
- MSG_BROADCAST_START = "📣 **Starting Broadcast...**\n\n> ⏳ Please wait for completion."
274
- MSG_BROADCAST_COMPLETE = (
275
- "📢 **Broadcast Completed Successfully!** 📢\n\n"
276
- "⏱️ **Duration:** `{elapsed_time}`\n"
277
- "👥 **Total Users:** `{total_users}`\n"
278
- "✅ **Successful Deliveries:** `{successes}`\n"
279
- "❌ **Failed Deliveries:** `{failures}`\n"
280
- "🗑️ **Accounts Removed (Blocked/Deactivated):** `{deleted_accounts}`\n"
281
- )
282
- MSG_BROADCAST_CANCEL = "🛑 **Cancelling Broadcast:** `{broadcast_id}`\n\n> ⏳ Stopping operations..."
283
- MSG_INVALID_BROADCAST_CMD = "Please reply to the message you want to broadcast."
284
- MSG_BROADCAST_USAGE = (
285
- "📣 **Broadcast Command Usage:**\n\n"
286
- "`/broadcast` - Broadcast to all users\n"
287
- "`/broadcast authorized` - Broadcast to authorized users only\n"
288
- "`/broadcast regular` - Broadcast to regular (non-authorized) users only\n\n"
289
- "**Note:** Reply to the message you want to broadcast."
290
- )
291
-
292
- # =====================================================================================
293
- # ====== PERMISSION MESSAGES ======
294
- # =====================================================================================
295
-
296
- MSG_ERROR_UNAUTHORIZED = "You are not authorized to view this information."
297
- MSG_ERROR_BROADCAST_RESTART = "Please use the /broadcast command to start a new broadcast."
298
- MSG_ERROR_BROADCAST_INSTRUCTION = "To start a new broadcast, use the /broadcast command and reply to the message you want to broadcast."
299
- MSG_ERROR_CALLBACK_UNSUPPORTED = "This button is not active or no longer supported."
300
-
301
- # =====================================================================================
302
- # ====== RATE LIMITING MESSAGES ======
303
- # =====================================================================================
304
-
305
- MSG_RATE_LIMIT_QUEUE_PRIORITY = (
306
- "⚡ You're in the **Priority Queue!**\n\n"
307
- "> ⏳ **Estimated Wait:** `~{wait_estimate} minute{s}`\n"
308
- "> 🚀 **Status:** In Queue"
309
- )
310
-
311
- MSG_RATE_LIMIT_QUEUE_REGULAR = (
312
- "⏳ **Rate Limit Reached!**\n\n"
313
- "> ⌛ **Estimated Wait:** `~{wait_estimate} minute{s1}`\n"
314
- "> 📊 **Limit:** `{max_requests} files per {time_window} minute{s2}`\n"
315
- "> 🔄 **Status:** In Queue"
316
- )
317
-
318
- MSG_RATE_LIMIT_QUEUE_FULL = (
319
- "⚠️ **Service Busy!** The processing queue is currently full.\n\n"
320
- "> 🕒 **Please try again in:** `~{wait_estimate} minute{s}`\n"
321
- "> 💡 **Tip:** Try again later when system load decreases"
322
- )
323
-
324
-
325
- # =====================================================================================
326
- # ====== FILE TYPE DESCRIPTIONS ======
327
- # =====================================================================================
328
- MSG_FILE_TYPE_DOCUMENT = "📄 Document"
329
- MSG_FILE_TYPE_PHOTO = "🖼️ Photo"
330
- MSG_FILE_TYPE_VIDEO = "🎬 Video"
331
- MSG_FILE_TYPE_AUDIO = "🎵 Audio"
332
- MSG_FILE_TYPE_VOICE = "🎤 Voice Message"
333
- MSG_FILE_TYPE_STICKER = "🎨 Sticker"
334
- MSG_FILE_TYPE_ANIMATION = "🎞️ Animation (GIF)"
335
- MSG_FILE_TYPE_VIDEO_NOTE = "📹 Video Note"
336
- MSG_FILE_TYPE_UNKNOWN = "❓ Unknown File Type"
337
-
338
- # =====================================================================================
339
- # ====== SYSTEM & STATUS MESSAGES ======
340
- # =====================================================================================
341
-
342
- MSG_SYSTEM_STATUS = (
343
- "✅ **System Status:** Operational\n\n"
344
- "> 🕒 **Uptime:** `{uptime}`\n"
345
- "> 🤖 **Bot Instances:** `{active_bots}`\n"
346
- "> 📊 **Total Workload:** `{total_workload}`\n\n"
347
- "📜 **Workload Distribution:**\n\n"
348
- "{workload_items}\n"
349
- "> ♻️ **Version:** `{version}`"
350
- )
351
-
352
- # ------ Speedtest Messages ------
353
- MSG_SPEEDTEST_INIT = "🚀 **Running Speed Test...**"
354
- MSG_SPEEDTEST_ERROR = "❌ **Speed Test Failed!**\n\n> Unable to complete the speed test. Please try again later."
355
- MSG_SPEEDTEST_RESULT = (
356
- "⚡ **Speed Test Results**\n\n"
357
- "**SPEEDTEST INFO:**\n"
358
- "> **Download:** `{download_mbps} Mbps` (`{download_bps}/s`)\n"
359
- "> **Upload:** `{upload_mbps} Mbps` (`{upload_bps}/s`)\n"
360
- "> **Ping:** `{ping} ms`\n"
361
- "> **Timestamp:** `{timestamp}`\n"
362
- "> **Data Sent:** `{bytes_sent}`\n"
363
- "> **Data Received:** `{bytes_received}`\n\n"
364
- "**SERVER INFO:**\n"
365
- "> **Name:** `{server_name}`\n"
366
- "> **Country:** `{server_country}`\n"
367
- "> **Sponsor:** `{server_sponsor}`\n"
368
- "> **Latency:** `{server_latency} ms`\n"
369
- "> **Coordinates:** `{server_lat}, {server_lon}`\n\n"
370
- "**CLIENT DETAILS:**\n"
371
- "> **IP:** `{client_ip}`\n"
372
- "> **Coordinates:** `{client_lat}, {client_lon}`\n"
373
- "> **ISP:** `{client_isp}`\n"
374
- "> **ISP Rating:** `{client_isprating}`\n"
375
- "> **Country:** `{client_country}`"
376
- )
377
- MSG_SYSTEM_STATS = (
378
- "📊 **System Statistics**\n\n"
379
- "> System Uptime: {sys_uptime}\n"
380
- "> Bot Uptime: {bot_uptime}\n\n"
381
- "⚙️ **Performance:**\n"
382
- "> CPU: {cpu_percent}%\n"
383
- "> CPU Core: {cpu_cores}\n"
384
- "> Frequency: {cpu_freq} GHz\n\n"
385
- "💾 **RAM**\n"
386
- "> Total: {ram_total}\n"
387
- "> Used: {ram_used}\n"
388
- "> Free: {ram_free}\n\n"
389
- "💽 **Storage:**\n"
390
- "> Disk: `{disk_percent}%`\n"
391
- "> Total: `{total}`\n"
392
- "> Used: `{used}`\n"
393
- "> Free: `{free}`\n\n"
394
- "📶 **Network:**\n"
395
- "> 🔺 Upload: `{upload}`\n"
396
- "> 🔻 Download: `{download}`\n"
397
- )
398
-
399
- MSG_DB_STATS = "📊 **Database Statistics**\n\n> 👥 **Total Users:** `{total_users}`"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Thunder/utils/messages.py
2
+
3
+ # =====================================================================================
4
+ # ====== ERROR MESSAGES ======
5
+ # =====================================================================================
6
+
7
+ # ------ General Errors ------
8
+ MSG_ERROR_GENERIC = "⚠️ **Oops!** Something went wrong. Please try again. If the issue persists, contact support."
9
+ MSG_ERROR_USER_INFO = "❗ **User Not Found:** Couldn't find user. Please check the ID or Username."
10
+
11
+ # ------ User Input & Validation Errors ------
12
+ MSG_INVALID_USER_ID = "❌ **Invalid User ID:** Please provide a numeric user ID."
13
+ MSG_ERROR_START_BOT = "⚠️ You need to start the bot in private first to use this command.\n👉 [Click here]({invite_link}) to start a private chat."
14
+ MSG_ERROR_REPLY_FILE = "⚠️ Please use the /link command in reply to a file."
15
+ MSG_ERROR_NO_FILE = "⚠️ The message you're replying to does not contain any file."
16
+ MSG_ERROR_INVALID_NUMBER = "⚠️ **Invalid number specified.**"
17
+ MSG_ERROR_NUMBER_RANGE = "⚠️ **Please specify a number between 1 and {max_files}.**"
18
+ MSG_ERROR_DM_FAILED = "⚠️ I couldn't send you a Direct Message. Please start the bot first."
19
+
20
+ # ------ File & Media Errors ------
21
+ MSG_ERROR_PROCESSING_MEDIA = "⚠️ **Oops!** Something went wrong while processing your media. Please try again. If the issue persists, contact support."
22
+
23
+ # ------ Admin Action Errors (Ban, Auth, etc.) ------
24
+ MSG_AUTHORIZE_FAILED = (
25
+ "❌ **Authorization Failed:** "
26
+ "Could not authorize user `{user_id}`."
27
+ )
28
+ MSG_DEAUTHORIZE_FAILED = (
29
+ "❌ **Deauthorization Failed:** "
30
+ "User `{user_id}` was not authorized or an error occurred."
31
+ )
32
+ MSG_TOKEN_FAILED = (
33
+ "⚠️ **Token Activation Failed!**\n\n"
34
+ "> ❗ Reason: {reason}\n\n"
35
+ "🔑 Please check your token or contact support."
36
+ )
37
+ MSG_SHELL_ERROR = """**❌ Shell Command Error ❌**
38
+ <pre>{error}</pre>"""
39
+
40
+ # ------ System & Bot Errors ------
41
+ MSG_ERROR_NOT_ADMIN = "⚠️ **Admin Required:** I need admin privileges to work here."
42
+ MSG_DC_INVALID_USAGE = "🤔 **Invalid Usage:** Please reply to a user's message or a media file to get DC info."
43
+ MSG_DC_ANON_ERROR = "😥 **Cannot Get Your DC Info:** Unable to identify you. This command might not work for anonymous users."
44
+ MSG_DC_FILE_ERROR = "⚙️ **Error Getting File DC Info:** Could not fetch details. File might be inaccessible."
45
+ MSG_STATS_ERROR = "❌ **Stats Error:** Could not retrieve system statistics."
46
+ MSG_STATUS_ERROR = "❌ **Status Error:** Could not retrieve system status."
47
+ MSG_DB_ERROR = "❌ **Database Error:** Could not retrieve user count."
48
+ MSG_CRITICAL_ERROR = (
49
+ "🚨 **Critical Media Processing Error** 🚨\n\n"
50
+ "> ⚠️ Details:\n```\n{error}\n```\n\n"
51
+ "Please investigate immediately! (ID: {error_id})"
52
+ )
53
+
54
+ # =====================================================================================
55
+ # ====== ADMIN MESSAGES ======
56
+ # =====================================================================================
57
+
58
+ # ------ Ban/Unban ------
59
+ MSG_DECORATOR_BANNED = "You are currently banned and cannot use this bot.\nReason: {reason}\nBanned on: {ban_time}"
60
+ MSG_BAN_USAGE = "⚠️ **Usage:** /ban [user_id] [reason]"
61
+ MSG_CANNOT_BAN_OWNER = "❌ **Cannot ban an owner.**"
62
+ MSG_ADMIN_USER_BANNED = "✅ **User {user_id} has been banned."
63
+ MSG_BAN_REASON_SUFFIX = "\n📝 **Reason:** {reason}"
64
+ MSG_ADMIN_NO_BAN_REASON = "No reason provided"
65
+ MSG_USER_BANNED_NOTIFICATION = "🚫 **You have been banned from using this bot.**"
66
+ MSG_UNBAN_USAGE = "⚠️ **Usage:** /unban <user_id>"
67
+ MSG_ADMIN_USER_UNBANNED = "✅ **User {user_id} has been unbanned."
68
+ MSG_USER_UNBANNED_NOTIFICATION = "🎉 **You have been unbanned from using this bot.**"
69
+ MSG_USER_NOT_IN_BAN_LIST = "ℹ️ **User {user_id} was not found in the ban list."
70
+ MSG_CHANNEL_BANNED = "✅ **Channel {channel_id} has been banned.**"
71
+ MSG_CHANNEL_BANNED_REASON_SUFFIX = "\n📝 **Reason:** {reason}"
72
+ MSG_CHANNEL_UNBANNED = "✅ **Channel {channel_id} has been unbanned.**"
73
+ MSG_CHANNEL_NOT_BANNED = "ℹ️ **Channel {channel_id} was not found in the ban list.**"
74
+
75
+ # ------ Token & Authorization ------
76
+ MSG_AUTHORIZE_USAGE = "🔑 **Usage:** `/authorize <user_id>`"
77
+ MSG_DEAUTHORIZE_USAGE = "🔒 **Usage:** `/deauthorize <user_id>`"
78
+ MSG_AUTHORIZE_SUCCESS = (
79
+ "✅ **User Authorized!**\n\n"
80
+ "> 👤 User ID: `{user_id}`\n"
81
+ "> 🔑 Access: Permanent"
82
+ )
83
+ MSG_DEAUTHORIZE_SUCCESS = (
84
+ "✅ **User Deauthorized!**\n\n"
85
+ "> 👤 User ID: `{user_id}`\n"
86
+ "> 🔒 Access: Revoked"
87
+ )
88
+ MSG_TOKEN_ACTIVATED = "✅ Token successfully activated!\n\n⏳ This token is valid for {duration_hours} hours."
89
+ MSG_TOKEN_INVALID = "🚫 **Expired or Invalid Token.** Please click the button below to activate your access token."
90
+ MSG_NO_AUTH_USERS = "ℹ️ **No Authorized Users Found:** The list is currently empty."
91
  MSG_AUTH_USER_INFO = """{i}. 👤: {display_name}
92
  • User ID: `{user_id}`
93
  • Authorized by: `{authorized_by}`
94
  • Date: `{auth_time}`\n\n"""
95
+ MSG_ADMIN_AUTH_LIST_HEADER = "🔐 **Authorized Users List**\n\n"
96
+
97
+ # ------ Shell Commands ------
98
+ MSG_SHELL_USAGE = (
99
+ "<b>Usage:</b>\n"
100
+ "/shell <command>\n\n"
101
+ "<b>Example:</b>\n"
102
+ "/shell ls -l"
103
+ )
104
+ MSG_SHELL_EXECUTING = "Executing Command... ⚙️\n<pre>{command}</pre>"
105
+ MSG_SHELL_OUTPUT = """**Shell Command Output:**
106
+ <pre>{output}</pre>"""
107
+ MSG_SHELL_OUTPUT_STDOUT = "<b>[stdout]:</b>\n<pre>{output}</pre>"
108
+ MSG_SHELL_OUTPUT_STDERR = "<b>[stderr]:</b>\n<pre>{error}</pre>"
109
+ MSG_SHELL_NO_OUTPUT = "✅ <b>Command Executed:</b> No output."
110
+
111
+ # ------ Admin View & Control ------
112
+
113
+ MSG_WORKLOAD_ITEM = " {bot_name}: {load}\n"
114
+ MSG_ADMIN_RESTART_DONE = "✅ **Restart Successful!**"
115
+ MSG_RESTARTING = "♻️ **Updating and Restarting Bot...**\n\n> ⏳ Please wait a moment."
116
+ MSG_LOG_FILE_CAPTION = "📄 **System Logs**"
117
+
118
+ MSG_LOG_FILE_EMPTY = "ℹ️ **Log File Empty:** No data found in the log file."
119
+ MSG_LOG_FILE_MISSING = "⚠️ **Log File Missing:** Could not find the log file."
120
+
121
+ # =====================================================================================
122
+ # ====== BUTTON TEXTS (User-facing) ======
123
+ # =====================================================================================
124
+
125
+ MSG_BUTTON_STREAM_NOW = "🖥️ Stream"
126
+ MSG_BUTTON_DOWNLOAD = "🚀 Download"
127
+ MSG_BUTTON_GET_HELP = "📖 Get Help"
128
+ MSG_BUTTON_CANCEL_BROADCAST = "🛑 Cancel Broadcast"
129
+ MSG_BUTTON_VIEW_PROFILE = "👤 View User Profile"
130
+ MSG_BUTTON_ABOUT = "ℹ️ About Bot"
131
+ MSG_BUTTON_JOIN_CHANNEL = "📢 Join {channel_title}"
132
+ MSG_BUTTON_GITHUB = "🛠️ GitHub"
133
+ MSG_BUTTON_START_CHAT = "📩 Start Chat"
134
+ MSG_BUTTON_CLOSE = "✖ Close"
135
+
136
+
137
+ # =====================================================================================
138
+ # ====== COMMAND RESPONSES (User-facing) ======
139
+ # =====================================================================================
140
+
141
+ MSG_WELCOME = (
142
+ "🌟 **Welcome, {user_name}!** 🌟\n\n"
143
+ "I'm **Flash File** ⚡\n"
144
+ "I generate direct download and streaming links for your files.\n\n"
145
+ "**How to use:**\n"
146
+ "1. Send any file to me for private links.\n"
147
+ "2. In groups, reply to a file with `/link`.\n\n"
148
+ "» Use `/help` for all commands and detailed information.\n\n"
149
+ "🚀 Send a file to begin!"
150
+ )
151
+
152
+ MSG_HELP = (
153
+ "📘 **Flash File - Help Guide** 📖\n\n"
154
+ "How to get direct download & streaming links:\n\n"
155
+ "**🚀 Private Chat (with me):**\n"
156
+ "> 1. Send me **any file** (document, video, audio, photo, etc.).\n"
157
+ "> 2. I'll instantly reply with your links! ⚡\n\n"
158
+ "**👥 Using in Groups:**\n"
159
+ "> • Reply to any file with `/link`.\n"
160
+ "> • **Batch Mode:** Reply to the **first** file with `/link <number>` (e.g., `/link 5` for 5 files, up to {max_files}).\n"
161
+ "> • Bot needs administrator rights in the group to function.\n"
162
+ "> • Links are posted in the group & sent to you privately.\n\n"
163
+ "**📢 Using in Channels:**\n"
164
+ "> • Add me as an administrator with necessary permissions.\n"
165
+ "> • I can be configured to auto-detect new media files.\n"
166
+ "> • Inline stream/download buttons can be added to files automatically.\n"
167
+ "> • Files from banned channels (owner configuration) are rejected.\n"
168
+ "> • Auto-posting links if the bot has admin privileges with delete rights.\n\n"
169
+ "**⚙️ Available Commands:**\n"
170
+ "> `/start` 👋 - Welcome message & quick start information.\n"
171
+ "> `/help` 📖 - Shows this help message.\n"
172
+ "> `/link <num>` 🔗 - (Groups) Generate links. \n"
173
+ "> `/about` ℹ️ - Learn more about me and my features.\n"
174
+ "> `/ping` 📡 - Check my responsiveness and online status.\n"
175
+ "> `/dc` 🌍 - View DC information (for yourself, another user, or a file).\n\n"
176
+ "**💡 Pro Tips:**\n"
177
+ "> • You can forward files from other chats directly to me.\n"
178
+ "> • If you encounter a rate limit message, please wait the specified time. ⏳\n"
179
+ "> • For `/link` in groups to work reliably (and for private link delivery), ensure you've started a private chat with me first.\n"
180
+ "> • Processing batch files might take a bit longer. Please be patient. 🐌\n\n"
181
+ "❓ Questions? Please ask in our support group!"
182
+ )
183
+
184
+ MSG_ABOUT = (
185
+ "🌟 **About Flash File - File to Link Bot** ℹ️\n\n"
186
+ "I'm your go-to bot for **instant download & streaming!** ⚡\n\n"
187
+ "**🚀 Key Features:**\n"
188
+ "> **Instant Links:** Get your links within seconds.\n"
189
+ "> **Online Streaming:** Watch videos or listen to audio directly (for supported formats).\n"
190
+ "> **Universal File Support:** Handles documents, videos, audio, photos, and more.\n"
191
+ "> **High-Speed Access:** Optimized for fast link generation and file access.\n"
192
+ "> **Secure & Reliable:** Your files are handled with care during processing.\n"
193
+ "> **User-Friendly Interface:** Designed for ease of use on any device.\n"
194
+ "> **Efficient Processing:** Built for speed and reliability.\n"
195
+ "> **Batch Mode:** Process multiple files at once in groups using `/link <number>`.\n"
196
+ "> **Versatile Usage:** Works in private chats, groups, and channels (with admin setup).\n\n"
197
+ "💖 If you find me useful, please consider sharing me with your friends!"
198
+ )
199
+
200
+ # ------ Ping ------
201
+ MSG_PING_START = "🛰️ **Pinging...** Please wait."
202
+ MSG_PING_RESPONSE = (
203
+ "☁️ **PONG! Bot is Online!** ⚡\n\n"
204
+ "> ⏱️ **Ping:** {time_taken_ms:.2f} ms\n"
205
+ "> 🤖 **Bot Status:** `Active`"
206
+ )
207
+
208
+ # ------ DC Info ------
209
+ MSG_DC_USER_INFO = (
210
+ "📍 **Information**\n"
211
+ "> 👤 **User:** [{user_name}](tg://user?id={user_id})\n"
212
+ "> 🆔 **User ID:** `{user_id}`\n"
213
+ "> 🌍 **DC ID:** `{dc_id}`"
214
+ )
215
+
216
+ MSG_DC_FILE_INFO = (
217
+ "🗂️ **File Information**\n"
218
+ ">`{file_name}`\n"
219
+ "💾 **File Size:** `{file_size}`\n"
220
+ "📁 **File Type:** `{file_type}`\n"
221
+ "🌍 **DC ID:** `{dc_id}`"
222
+ )
223
+
224
+ MSG_DC_UNKNOWN = "Unknown"
225
+
226
+ # ------ File Link Generation ------
227
+ MSG_DM_SINGLE_PREFIX = "📬 **From {chat_title}**\n"
228
+ MSG_LINKS = (
229
+ "✨ **Your Links are Ready!** ✨\n\n"
230
+ "> `{file_name}`\n\n"
231
+ "📂 **File Size:** `{file_size}`\n\n"
232
+ "🚀 **Download Link:**\n`{download_link}`\n\n"
233
+ "🖥️ **Stream Link:**\n`{stream_link}`\n\n"
234
+ "⌛️ **Note: Links remain active while the bot is running and the file is accessible.**"
235
+ )
236
+
237
+ # =====================================================================================
238
+ # ====== USER NOTIFICATIONS ======
239
+ # =====================================================================================
240
+
241
+ MSG_NEW_USER = (
242
+ "✨ **New User Alert!** ✨\n"
243
+ "> 👤 **Name:** [{first_name}](tg://user?id={user_id})\n"
244
+ "> 🆔 **User ID:** `{user_id}`\n\n"
245
+ )
246
+ MSG_COMMUNITY_CHANNEL = "📢 **{channel_title}:** 🔒 Join this channel to use the bot."
247
+
248
+ # =====================================================================================
249
+ # ====== PROCESSING MESSAGES ======
250
+ # =====================================================================================
251
+
252
+ # ------ General File Processing ------
253
+ MSG_PROCESSING_REQUEST = "⏳ **Processing your request...**"
254
+ MSG_PROCESSING_FILE = "⏳ **Processing your file...**"
255
+ MSG_NEW_FILE_REQUEST = (
256
+ "> 👤 **Source:** [{source_info}](tg://user?id={id_})\n"
257
+ "> 🆔 **ID:** `{id_}`\n\n"
258
+ "🚀 **Download:** `{online_link}`\n\n"
259
+ "🖥️ **Stream:** `{stream_link}`"
260
+ )
261
+
262
+ # ------ Batch Processing ------
263
+ MSG_PROCESSING_BATCH = "♻️ **Processing Batch {batch_number}/{total_batches}** ({file_count} files)"
264
+ MSG_PROCESSING_STATUS = "📊 **Processing Files:** {processed}/{total} complete, {failed} failed"
265
+ MSG_BATCH_LINKS_READY = "🔗 Here are your {count} download links:"
266
+ MSG_DM_BATCH_PREFIX = "📬 **Batch Links from {chat_title}**\n"
267
+ MSG_PROCESSING_RESULT = "✅ **Process Complete:** {processed}/{total} files processed successfully, {failed} failed"
268
+
269
+ # =====================================================================================
270
+ # ====== BROADCAST MESSAGES ======
271
+ # =====================================================================================
272
+
273
+ MSG_BROADCAST_START = "📣 **Starting Broadcast...**\n\n> ⏳ Please wait for completion."
274
+ MSG_BROADCAST_COMPLETE = (
275
+ "📢 **Broadcast Completed Successfully!** 📢\n\n"
276
+ "⏱️ **Duration:** `{elapsed_time}`\n"
277
+ "👥 **Total Users:** `{total_users}`\n"
278
+ "✅ **Successful Deliveries:** `{successes}`\n"
279
+ "❌ **Failed Deliveries:** `{failures}`\n"
280
+ "🗑️ **Accounts Removed (Blocked/Deactivated):** `{deleted_accounts}`\n"
281
+ )
282
+ MSG_BROADCAST_CANCEL = "🛑 **Cancelling Broadcast:** `{broadcast_id}`\n\n> ⏳ Stopping operations..."
283
+ MSG_INVALID_BROADCAST_CMD = "Please reply to the message you want to broadcast."
284
+ MSG_BROADCAST_USAGE = (
285
+ "📣 **Broadcast Command Usage:**\n\n"
286
+ "`/broadcast` - Broadcast to all users\n"
287
+ "`/broadcast authorized` - Broadcast to authorized users only\n"
288
+ "`/broadcast regular` - Broadcast to regular (non-authorized) users only\n\n"
289
+ "**Note:** Reply to the message you want to broadcast."
290
+ )
291
+
292
+ # =====================================================================================
293
+ # ====== PERMISSION MESSAGES ======
294
+ # =====================================================================================
295
+
296
+ MSG_ERROR_UNAUTHORIZED = "You are not authorized to view this information."
297
+ MSG_ERROR_BROADCAST_RESTART = "Please use the /broadcast command to start a new broadcast."
298
+ MSG_ERROR_BROADCAST_INSTRUCTION = "To start a new broadcast, use the /broadcast command and reply to the message you want to broadcast."
299
+ MSG_ERROR_CALLBACK_UNSUPPORTED = "This button is not active or no longer supported."
300
+
301
+ # =====================================================================================
302
+ # ====== RATE LIMITING MESSAGES ======
303
+ # =====================================================================================
304
+
305
+ MSG_RATE_LIMIT_QUEUE_PRIORITY = (
306
+ "⚡ You're in the **Priority Queue!**\n\n"
307
+ "> ⏳ **Estimated Wait:** `~{wait_estimate} minute{s}`\n"
308
+ "> 🚀 **Status:** In Queue"
309
+ )
310
+
311
+ MSG_RATE_LIMIT_QUEUE_REGULAR = (
312
+ "⏳ **Rate Limit Reached!**\n\n"
313
+ "> ⌛ **Estimated Wait:** `~{wait_estimate} minute{s1}`\n"
314
+ "> 📊 **Limit:** `{max_requests} files per {time_window} minute{s2}`\n"
315
+ "> 🔄 **Status:** In Queue"
316
+ )
317
+
318
+ MSG_RATE_LIMIT_QUEUE_FULL = (
319
+ "⚠️ **Service Busy!** The processing queue is currently full.\n\n"
320
+ "> 🕒 **Please try again in:** `~{wait_estimate} minute{s}`\n"
321
+ "> 💡 **Tip:** Try again later when system load decreases"
322
+ )
323
+
324
+
325
+ # =====================================================================================
326
+ # ====== FILE TYPE DESCRIPTIONS ======
327
+ # =====================================================================================
328
+ MSG_FILE_TYPE_DOCUMENT = "📄 Document"
329
+ MSG_FILE_TYPE_PHOTO = "🖼️ Photo"
330
+ MSG_FILE_TYPE_VIDEO = "🎬 Video"
331
+ MSG_FILE_TYPE_AUDIO = "🎵 Audio"
332
+ MSG_FILE_TYPE_VOICE = "🎤 Voice Message"
333
+ MSG_FILE_TYPE_STICKER = "🎨 Sticker"
334
+ MSG_FILE_TYPE_ANIMATION = "🎞️ Animation (GIF)"
335
+ MSG_FILE_TYPE_VIDEO_NOTE = "📹 Video Note"
336
+ MSG_FILE_TYPE_UNKNOWN = "❓ Unknown File Type"
337
+
338
+ # =====================================================================================
339
+ # ====== SYSTEM & STATUS MESSAGES ======
340
+ # =====================================================================================
341
+
342
+ MSG_SYSTEM_STATUS = (
343
+ "✅ **System Status:** Operational\n\n"
344
+ "> 🕒 **Uptime:** `{uptime}`\n"
345
+ "> 🤖 **Bot Instances:** `{active_bots}`\n"
346
+ "> 📊 **Total Workload:** `{total_workload}`\n\n"
347
+ "📜 **Workload Distribution:**\n\n"
348
+ "{workload_items}\n"
349
+ "> ♻️ **Version:** `{version}`"
350
+ )
351
+
352
+ # ------ Speedtest Messages ------
353
+ MSG_SPEEDTEST_INIT = "🚀 **Running Speed Test...**"
354
+ MSG_SPEEDTEST_ERROR = "❌ **Speed Test Failed!**\n\n> Unable to complete the speed test. Please try again later."
355
+ MSG_SPEEDTEST_RESULT = (
356
+ "⚡ **Speed Test Results**\n\n"
357
+ "**SPEEDTEST INFO:**\n"
358
+ "> **Download:** `{download_mbps} Mbps` (`{download_bps}/s`)\n"
359
+ "> **Upload:** `{upload_mbps} Mbps` (`{upload_bps}/s`)\n"
360
+ "> **Ping:** `{ping} ms`\n"
361
+ "> **Timestamp:** `{timestamp}`\n"
362
+ "> **Data Sent:** `{bytes_sent}`\n"
363
+ "> **Data Received:** `{bytes_received}`\n\n"
364
+ "**SERVER INFO:**\n"
365
+ "> **Name:** `{server_name}`\n"
366
+ "> **Country:** `{server_country}`\n"
367
+ "> **Sponsor:** `{server_sponsor}`\n"
368
+ "> **Latency:** `{server_latency} ms`\n"
369
+ "> **Coordinates:** `{server_lat}, {server_lon}`\n\n"
370
+ "**CLIENT DETAILS:**\n"
371
+ "> **IP:** `{client_ip}`\n"
372
+ "> **Coordinates:** `{client_lat}, {client_lon}`\n"
373
+ "> **ISP:** `{client_isp}`\n"
374
+ "> **ISP Rating:** `{client_isprating}`\n"
375
+ "> **Country:** `{client_country}`"
376
+ )
377
+ MSG_SYSTEM_STATS = (
378
+ "📊 **System Statistics**\n\n"
379
+ "> System Uptime: {sys_uptime}\n"
380
+ "> Bot Uptime: {bot_uptime}\n\n"
381
+ "⚙️ **Performance:**\n"
382
+ "> CPU: {cpu_percent}%\n"
383
+ "> CPU Core: {cpu_cores}\n"
384
+ "> Frequency: {cpu_freq} GHz\n\n"
385
+ "💾 **RAM**\n"
386
+ "> Total: {ram_total}\n"
387
+ "> Used: {ram_used}\n"
388
+ "> Free: {ram_free}\n\n"
389
+ "💽 **Storage:**\n"
390
+ "> Disk: `{disk_percent}%`\n"
391
+ "> Total: `{total}`\n"
392
+ "> Used: `{used}`\n"
393
+ "> Free: `{free}`\n\n"
394
+ "📶 **Network:**\n"
395
+ "> 🔺 Upload: `{upload}`\n"
396
+ "> 🔻 Download: `{download}`\n"
397
+ )
398
+
399
+ MSG_DB_STATS = "📊 **Database Statistics**\n\n> 👥 **Total Users:** `{total_users}`"
400
+
401
+ # Payment & Subscription Messages
402
+ MSG_SUB_PLAN_ACTIVE = (
403
+ "👑 **Flash File Premium Active**\n\n"
404
+ "👤 **Account:** `{user_name}` (`{user_id}`)\n"
405
+ "📅 **Valid Until:** `{expiry_date}`\n"
406
+ "⏳ **Remaining:** `{remaining_days} days`\n"
407
+ "⚡ **Tier:** `30-Day Monthly Pass (Unlimited)`\n\n"
408
+ "✨ You have unlimited streaming & high-speed direct downloads!"
409
+ )
410
+
411
+ MSG_SUB_PLAN_FREE = (
412
+ "⚡ **Flash File Premium Access**\n\n"
413
+ "Unlock unlimited ultra-fast streaming & direct download links:\n\n"
414
+ "💳 **Monthly Pass:** `₹{price} / 30 Days`\n"
415
+ "✨ Unlimited high-speed streaming links\n"
416
+ "⚡ Pure Google Pay UPI integration\n"
417
+ "📱 Compatible with VLC, MX Player, and Web\n"
418
+ "🎟 Optional 1-time file passes available\n\n"
419
+ "🔗 **UPI ID:** `{upi_id}`"
420
+ )
421
+
422
+ MSG_PAYWALL_GATE = (
423
+ "🔒 **Flash File Premium Access Required**\n\n"
424
+ "Direct streaming links for this bot require an active pass:\n\n"
425
+ "💎 **Monthly Pass:** `₹{price} / 30 Days` (Unlimited)\n"
426
+ "🎟 **1-Time Pass:** Request temporary access for this file\n\n"
427
+ "📲 **Pay via UPI:** `{upi_id}`\n"
428
+ "*(Supports Google Pay, PhonePe, Paytm, BHIM)*"
429
+ )
430
+
431
+ MSG_PROOF_INSTRUCTION = (
432
+ "📩 **Submit Payment Proof**\n\n"
433
+ "Please send your **Transaction ID / UTR Number** or reply to this message with a **screenshot of your payment**.\n\n"
434
+ "Admin will verify and activate your pass within minutes."
435
+ )
436
+
437
+ MSG_SUB_APPROVED_USER = (
438
+ "🎉 **Subscription Activated!**\n\n"
439
+ "Your **30-Day Monthly Pass** is now active!\n"
440
+ "📅 **Valid Until:** `{expiry_date}`\n\n"
441
+ "You can now send any video or document to generate instant stream and download links. Enjoy!"
442
+ )
443
+
444
+ MSG_TEMP_APPROVED_USER = (
445
+ "🎟 **Temporary Stream Pass Approved!**\n\n"
446
+ "You have been granted a **1-time temporary stream pass** (valid for 24 hours).\n\n"
447
+ "Send or forward your file again to generate your link!"
448
+ )
449
+
450
+ MSG_SUB_REJECTED_USER = (
451
+ "❌ **Access Request Not Approved**\n\n"
452
+ "Your payment proof or pass request could not be verified by admin.\n"
453
+ "If this is an error or you need help, please reach out to admin."
454
+ )
Thunder/vars.py CHANGED
@@ -1,111 +1,120 @@
1
- # Thunder/vars.py
2
-
3
- import os
4
-
5
- from dotenv import load_dotenv
6
- from typing import Set, Optional
7
- from Thunder.utils.logger import logger
8
-
9
- load_dotenv("config.env")
10
-
11
- def str_to_bool(val: str) -> bool:
12
- return val.lower() in ("true", "1", "t", "y", "yes")
13
-
14
- def str_to_int_set(val: str) -> Set[int]:
15
- if not val:
16
- return set()
17
- result: Set[int] = set()
18
- for x in val.split():
19
- try:
20
- result.add(int(x))
21
- except (TypeError, ValueError):
22
- continue
23
- return result
24
-
25
-
26
-
27
- class Var:
28
- raw_api_id = os.getenv("API_ID", "0").strip(" '\"")
29
- API_ID: int = int(raw_api_id) if raw_api_id.isdigit() else 0
30
- API_HASH: str = os.getenv("API_HASH", "").strip(" '\"")
31
- BOT_TOKEN: str = os.getenv("BOT_TOKEN", "").strip(" '\"")
32
-
33
- logger.info(
34
- "FileToLink Telegram configuration loaded "
35
- "(api_id=%s, api_hash=%s, bot_token=%s)",
36
- "set" if API_ID else "missing",
37
- "set" if API_HASH else "missing",
38
- "set" if BOT_TOKEN else "missing",
39
- )
40
-
41
- if not all([API_ID, API_HASH, BOT_TOKEN]):
42
- logger.critical("Missing required Telegram API configuration")
43
- raise ValueError("Missing required Telegram API configuration")
44
-
45
- NAME: str = os.getenv("NAME", "ThunderF2L")
46
- SLEEP_THRESHOLD: int = int(os.getenv("SLEEP_THRESHOLD", "600"))
47
- WORKERS: int = int(os.getenv("WORKERS", "8"))
48
-
49
- BIN_CHANNEL: int = int(os.getenv("BIN_CHANNEL", "0"))
50
-
51
- if not BIN_CHANNEL:
52
- logger.critical("BIN_CHANNEL is required")
53
- raise ValueError("BIN_CHANNEL is required")
54
-
55
- PORT: int = int(os.getenv("PORT", "8080"))
56
- BIND_ADDRESS: str = os.getenv("BIND_ADDRESS", "0.0.0.0")
57
- PING_INTERVAL: int = int(os.getenv("PING_INTERVAL", "840"))
58
- NO_PORT: bool = str_to_bool(os.getenv("NO_PORT", "True"))
59
-
60
- OWNER_ID: int = int(os.getenv("OWNER_ID", "0"))
61
-
62
- if not OWNER_ID:
63
- logger.warning("WARNING: OWNER_ID is not set. No user will be granted owner access.")
64
-
65
- FQDN: str = os.getenv("FQDN", "") or BIND_ADDRESS
66
- HAS_SSL: bool = str_to_bool(os.getenv("HAS_SSL", "True"))
67
- PROTOCOL: str = "https" if HAS_SSL else "http"
68
- PORT_SEGMENT: str = "" if NO_PORT else f":{PORT}"
69
- URL: str = f"{PROTOCOL}://{FQDN}{PORT_SEGMENT}/"
70
-
71
- SET_COMMANDS: bool = str_to_bool(os.getenv("SET_COMMANDS", "True"))
72
-
73
- DATABASE_URL: str = os.getenv("DATABASE_URL", "")
74
-
75
- if not DATABASE_URL:
76
- logger.critical("DATABASE_URL is required")
77
- raise ValueError("DATABASE_URL is required")
78
-
79
- MAX_BATCH_FILES: int = int(os.getenv("MAX_BATCH_FILES", "50"))
80
-
81
- CHANNEL: bool = str_to_bool(os.getenv("CHANNEL", "False"))
82
-
83
- BANNED_CHANNELS: Set[int] = str_to_int_set(os.getenv("BANNED_CHANNELS", ""))
84
-
85
- MULTI_CLIENT: bool = False
86
-
87
- FORCE_CHANNEL_ID: Optional[int] = None
88
-
89
- force_channel_env = os.getenv("FORCE_CHANNEL_ID", "").strip()
90
-
91
- if force_channel_env:
92
- try:
93
- FORCE_CHANNEL_ID = int(force_channel_env)
94
- except ValueError:
95
- logger.warning(f"Invalid FORCE_CHANNEL_ID '{force_channel_env}' in environment; must be an integer.")
96
-
97
- TOKEN_ENABLED: bool = str_to_bool(os.getenv("TOKEN_ENABLED", "False"))
98
- TOKEN_TTL_HOURS: int = int(os.getenv("TOKEN_TTL_HOURS", "24"))
99
-
100
- SHORTEN_ENABLED: bool = str_to_bool(os.getenv("SHORTEN_ENABLED", "False"))
101
- SHORTEN_MEDIA_LINKS: bool = str_to_bool(os.getenv("SHORTEN_MEDIA_LINKS", "False"))
102
- URL_SHORTENER_API_KEY: str = os.getenv("URL_SHORTENER_API_KEY", "")
103
- URL_SHORTENER_SITE: str = os.getenv("URL_SHORTENER_SITE", "")
104
-
105
- GLOBAL_RATE_LIMIT: bool = str_to_bool(os.getenv("GLOBAL_RATE_LIMIT", "False"))
106
- MAX_GLOBAL_REQUESTS_PER_MINUTE: int = int(os.getenv("MAX_GLOBAL_REQUESTS_PER_MINUTE", "4"))
107
-
108
- RATE_LIMIT_ENABLED: bool = str_to_bool(os.getenv("RATE_LIMIT_ENABLED", "False"))
109
- MAX_FILES_PER_PERIOD: int = int(os.getenv("MAX_FILES_PER_PERIOD", "2"))
110
- RATE_LIMIT_PERIOD_MINUTES: int = int(os.getenv("RATE_LIMIT_PERIOD_MINUTES", "1"))
111
- MAX_QUEUE_SIZE: int = int(os.getenv("MAX_QUEUE_SIZE", "100"))
 
 
 
 
 
 
 
 
 
 
1
+ # Thunder/vars.py
2
+
3
+ import os
4
+
5
+ from dotenv import load_dotenv
6
+ from typing import Set, Optional
7
+ from Thunder.utils.logger import logger
8
+
9
+ load_dotenv("config.env")
10
+
11
+ def str_to_bool(val: str) -> bool:
12
+ return val.lower() in ("true", "1", "t", "y", "yes")
13
+
14
+ def str_to_int_set(val: str) -> Set[int]:
15
+ if not val:
16
+ return set()
17
+ result: Set[int] = set()
18
+ for x in val.split():
19
+ try:
20
+ result.add(int(x))
21
+ except (TypeError, ValueError):
22
+ continue
23
+ return result
24
+
25
+
26
+
27
+ class Var:
28
+ raw_api_id = os.getenv("API_ID", "0").strip(" '\"")
29
+ API_ID: int = int(raw_api_id) if raw_api_id.isdigit() else 0
30
+ API_HASH: str = os.getenv("API_HASH", "").strip(" '\"")
31
+ BOT_TOKEN: str = os.getenv("BOT_TOKEN", "").strip(" '\"")
32
+
33
+ logger.info(
34
+ "FileToLink Telegram configuration loaded "
35
+ "(api_id=%s, api_hash=%s, bot_token=%s)",
36
+ "set" if API_ID else "missing",
37
+ "set" if API_HASH else "missing",
38
+ "set" if BOT_TOKEN else "missing",
39
+ )
40
+
41
+ if not all([API_ID, API_HASH, BOT_TOKEN]):
42
+ logger.critical("Missing required Telegram API configuration")
43
+ raise ValueError("Missing required Telegram API configuration")
44
+
45
+ NAME: str = os.getenv("NAME", "ThunderF2L")
46
+ SLEEP_THRESHOLD: int = int(os.getenv("SLEEP_THRESHOLD", "600"))
47
+ WORKERS: int = int(os.getenv("WORKERS", "8"))
48
+
49
+ BIN_CHANNEL: int = int(os.getenv("BIN_CHANNEL", "0"))
50
+
51
+ if not BIN_CHANNEL:
52
+ logger.critical("BIN_CHANNEL is required")
53
+ raise ValueError("BIN_CHANNEL is required")
54
+
55
+ PORT: int = int(os.getenv("PORT", "8080"))
56
+ BIND_ADDRESS: str = os.getenv("BIND_ADDRESS", "0.0.0.0")
57
+ PING_INTERVAL: int = int(os.getenv("PING_INTERVAL", "840"))
58
+ NO_PORT: bool = str_to_bool(os.getenv("NO_PORT", "True"))
59
+
60
+
61
+ raw_owner_id = os.getenv("OWNER_ID", "0").strip(" '\"")
62
+ OWNER_ID: int = int(raw_owner_id) if (raw_owner_id.isdigit() or (raw_owner_id.startswith("-") and raw_owner_id[1:].isdigit())) else 0
63
+ if not OWNER_ID:
64
+ logger.warning("WARNING: OWNER_ID is not set. No user will be granted owner access.")
65
+
66
+ FQDN: str = os.getenv("FQDN", "") or BIND_ADDRESS
67
+ HAS_SSL: bool = str_to_bool(os.getenv("HAS_SSL", "True"))
68
+ PROTOCOL: str = "https" if HAS_SSL else "http"
69
+ PORT_SEGMENT: str = "" if NO_PORT else f":{PORT}"
70
+ URL: str = f"{PROTOCOL}://{FQDN}{PORT_SEGMENT}/"
71
+
72
+ SET_COMMANDS: bool = str_to_bool(os.getenv("SET_COMMANDS", "True"))
73
+
74
+ DATABASE_URL: str = os.getenv("DATABASE_URL", "")
75
+
76
+ if not DATABASE_URL:
77
+ logger.critical("DATABASE_URL is required")
78
+ raise ValueError("DATABASE_URL is required")
79
+
80
+ MAX_BATCH_FILES: int = int(os.getenv("MAX_BATCH_FILES", "50"))
81
+
82
+ CHANNEL: bool = str_to_bool(os.getenv("CHANNEL", "False"))
83
+
84
+ BANNED_CHANNELS: Set[int] = str_to_int_set(os.getenv("BANNED_CHANNELS", ""))
85
+
86
+ MULTI_CLIENT: bool = False
87
+
88
+ FORCE_CHANNEL_ID: Optional[int] = None
89
+
90
+ force_channel_env = os.getenv("FORCE_CHANNEL_ID", "").strip()
91
+
92
+ if force_channel_env:
93
+ try:
94
+ FORCE_CHANNEL_ID = int(force_channel_env)
95
+ except ValueError:
96
+ logger.warning(f"Invalid FORCE_CHANNEL_ID '{force_channel_env}' in environment; must be an integer.")
97
+
98
+ TOKEN_ENABLED: bool = str_to_bool(os.getenv("TOKEN_ENABLED", "False"))
99
+ TOKEN_TTL_HOURS: int = int(os.getenv("TOKEN_TTL_HOURS", "24"))
100
+
101
+ SHORTEN_ENABLED: bool = str_to_bool(os.getenv("SHORTEN_ENABLED", "False"))
102
+ SHORTEN_MEDIA_LINKS: bool = str_to_bool(os.getenv("SHORTEN_MEDIA_LINKS", "False"))
103
+ URL_SHORTENER_API_KEY: str = os.getenv("URL_SHORTENER_API_KEY", "")
104
+ URL_SHORTENER_SITE: str = os.getenv("URL_SHORTENER_SITE", "")
105
+
106
+ GLOBAL_RATE_LIMIT: bool = str_to_bool(os.getenv("GLOBAL_RATE_LIMIT", "False"))
107
+ MAX_GLOBAL_REQUESTS_PER_MINUTE: int = int(os.getenv("MAX_GLOBAL_REQUESTS_PER_MINUTE", "4"))
108
+
109
+ RATE_LIMIT_ENABLED: bool = str_to_bool(os.getenv("RATE_LIMIT_ENABLED", "False"))
110
+ MAX_FILES_PER_PERIOD: int = int(os.getenv("MAX_FILES_PER_PERIOD", "2"))
111
+ RATE_LIMIT_PERIOD_MINUTES: int = int(os.getenv("RATE_LIMIT_PERIOD_MINUTES", "1"))
112
+ MAX_QUEUE_SIZE: int = int(os.getenv("MAX_QUEUE_SIZE", "100"))
113
+
114
+ # Subscription & Payment Gateway (Dormant by default)
115
+ PAYMENT_ENABLED: bool = str_to_bool(os.getenv("PAYMENT", os.getenv("Payment", "False")))
116
+ SUB_PRICE: int = int(os.getenv("SUB_PRICE", "10"))
117
+ SUB_DURATION_DAYS: int = int(os.getenv("SUB_DURATION_DAYS", "30"))
118
+ UPI_ID: str = os.getenv("UPI_ID", "jishnupg2005-2@okaxis").strip()
119
+ UPI_NAME: str = os.getenv("UPI_NAME", "Flash File").strip()
120
+ ADMIN_CHANNEL: int = int(os.getenv("ADMIN_CHANNEL", "0"))