Spaces:
Running
Running
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 +550 -515
- Thunder/bot/plugins/subscription.py +472 -0
- Thunder/utils/commands.py +43 -38
- Thunder/utils/database.py +625 -453
- Thunder/utils/messages.py +450 -395
- Thunder/vars.py +120 -111
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 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 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 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
return
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
await
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
(
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
await
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
(
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
from
|
| 6 |
-
from pymongo
|
| 7 |
-
from pymongo.
|
| 8 |
-
from
|
| 9 |
-
from Thunder.
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
self.
|
| 15 |
-
self.
|
| 16 |
-
self.
|
| 17 |
-
self.
|
| 18 |
-
self.
|
| 19 |
-
self.
|
| 20 |
-
self.
|
| 21 |
-
self.
|
| 22 |
-
self.
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
await self.col.create_index("id", unique=True)
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
await self.
|
| 55 |
-
await self.
|
| 56 |
-
await self.
|
| 57 |
-
await self.
|
| 58 |
-
await self.files_col.create_index("
|
| 59 |
-
await self.
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
return
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
return
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
return self.
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
except Exception as e:
|
| 186 |
-
logger.error(f"Error in
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
async def
|
| 190 |
-
try:
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
except Exception as e:
|
| 225 |
-
logger.error(f"Error in
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
async def
|
| 229 |
-
try:
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
logger.
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
async def
|
| 265 |
-
try:
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
)
|
| 444 |
-
return
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 61 |
-
|
| 62 |
-
if
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"))
|