Spaces:
Running
Running
Fyaz Mohammed commited on
1.9.2
Browse filesfeat: Centralize rate limiting and add auto-command setup
- Centralized rate limit handling to improve API reliability.
- Increased default sleep threshold from 120s to 300s.
- Added `SET_COMMANDS` option for automatic command registration.
- Refined bot command descriptions for clarity and consistency.
- Updated documentation for new features and command ordering.
- README.md +16 -10
- Thunder/__init__.py +1 -1
- Thunder/__main__.py +19 -29
- Thunder/bot/clients.py +10 -14
- Thunder/bot/plugins/admin.py +40 -61
- Thunder/bot/plugins/callbacks.py +31 -31
- Thunder/bot/plugins/common.py +20 -37
- Thunder/bot/plugins/stream.py +22 -33
- Thunder/server/stream_routes.py +5 -3
- Thunder/utils/bot_utils.py +12 -25
- Thunder/utils/broadcast.py +18 -26
- Thunder/utils/commands.py +37 -0
- Thunder/utils/custom_dl.py +29 -31
- Thunder/utils/decorators.py +21 -16
- Thunder/utils/file_properties.py +19 -11
- Thunder/utils/force_channel.py +21 -13
- Thunder/utils/handler.py +28 -0
- Thunder/utils/render_template.py +5 -3
- Thunder/vars.py +4 -2
- config_sample.env +4 -1
README.md
CHANGED
|
@@ -43,7 +43,8 @@
|
|
| 43 |
|
| 44 |
**Perfect for:**
|
| 45 |
|
| 46 |
-
-
|
|
|
|
| 47 |
- 🎬 Content creators sharing media files.
|
| 48 |
- 👥 Communities distributing resources.
|
| 49 |
- 🎓 Educational platforms sharing materials.
|
|
@@ -53,7 +54,7 @@
|
|
| 53 |
|
| 54 |
## ✨ Features
|
| 55 |
|
| 56 |
-
###
|
| 57 |
|
| 58 |
- **Generate Direct Links:** Convert Telegram media files into direct streaming links.
|
| 59 |
- **Permanent Links:** Links remain active as long as the file exists in the storage channel.
|
|
@@ -63,6 +64,7 @@
|
|
| 63 |
- **Channel and Group Support:** Works in private chats, groups, and channels.
|
| 64 |
- **MongoDB Integration:** Store user data and info with advanced database capabilities.
|
| 65 |
- **HTTP/HTTPS Streaming:** Stream media with custom player support for all devices and browsers.
|
|
|
|
| 66 |
|
| 67 |
### 🧩 Advanced Features
|
| 68 |
|
|
@@ -74,6 +76,7 @@
|
|
| 74 |
- **Custom Domain Support:** Use your own domain for streaming links.
|
| 75 |
- **Customizable Templates:** Personalize HTML templates for download pages.
|
| 76 |
- **Data Center Info:** Get data center information for users and files.
|
|
|
|
| 77 |
|
| 78 |
### ⚙️ Technical Capabilities
|
| 79 |
|
|
@@ -132,7 +135,7 @@ Rename `config_sample.env` to `config.env` and edit the following variables:
|
|
| 132 |
| `MULTI_BOT_TOKENS` | Additional bot tokens for load balancing | *(empty)* | `MULTI_TOKEN1=` |
|
| 133 |
| `FORCE_CHANNEL_ID` | Channel ID users must join | *(empty)* | `-1001234567890` |
|
| 134 |
| `BANNED_CHANNELS` | Space-separated banned channel IDs | *(empty)* | `-1001234567890 -100987654321`|
|
| 135 |
-
| `SLEEP_THRESHOLD` | Threshold for client switching | `
|
| 136 |
| `WORKERS` | Number of async workers | `8` | `200` |
|
| 137 |
| `NAME` | Bot application name | `ThunderF2L` | `MyFileBot` |
|
| 138 |
| `BIND_ADDRESS` | Address to bind web server | `0.0.0.0` | `127.0.0.1` |
|
|
@@ -143,6 +146,7 @@ Rename `config_sample.env` to `config.env` and edit the following variables:
|
|
| 143 |
| `SHORTEN_MEDIA_LINKS`| Enable URL shortening for media links | `False` | `True` |
|
| 144 |
| `URL_SHORTENER_API_KEY` | API key for URL shortening service | `""` | `"abc123def456"` |
|
| 145 |
| `URL_SHORTENER_SITE` | URL shortening service to use | `""` | `"example.com"` |
|
|
|
|
| 146 |
|
| 147 |
> ℹ️ For all options, see `config_sample.env`.
|
| 148 |
|
|
@@ -226,24 +230,26 @@ python -m Thunder
|
|
| 226 |
|
| 227 |
### Commands for @BotFather
|
| 228 |
|
| 229 |
-
|
|
|
|
|
|
|
| 230 |
|
| 231 |
```text
|
| 232 |
start - Start the bot and get a welcome message
|
| 233 |
-
link - Generate a direct link for a file
|
| 234 |
-
dc -
|
| 235 |
-
ping - Check
|
| 236 |
about - Get information about the bot
|
| 237 |
help - Show help and usage instructions
|
| 238 |
-
status - (Admin)
|
|
|
|
| 239 |
broadcast - (Admin) Send a message to all users
|
| 240 |
-
stats - (Admin) View usage statistics and analytics
|
| 241 |
ban - (Admin) Ban a user
|
| 242 |
unban - (Admin) Unban a user
|
| 243 |
log - (Admin) Send bot logs
|
| 244 |
restart - (Admin) Restart the bot
|
| 245 |
shell - (Admin) Execute a shell command
|
| 246 |
-
users - (Admin) Show total number of users
|
| 247 |
authorize - (Admin) Grant permanent access to a user
|
| 248 |
deauthorize - (Admin) Remove permanent access from a user
|
| 249 |
listauth - (Admin) List all authorized users
|
|
|
|
| 43 |
|
| 44 |
**Perfect for:**
|
| 45 |
|
| 46 |
+
- 🚀 Download Telegram media at high speed.
|
| 47 |
+
- ☁️ Leveraging free unlimited cloud storage with high-speed links.
|
| 48 |
- 🎬 Content creators sharing media files.
|
| 49 |
- 👥 Communities distributing resources.
|
| 50 |
- 🎓 Educational platforms sharing materials.
|
|
|
|
| 54 |
|
| 55 |
## ✨ Features
|
| 56 |
|
| 57 |
+
### 🧠 Core Functionality
|
| 58 |
|
| 59 |
- **Generate Direct Links:** Convert Telegram media files into direct streaming links.
|
| 60 |
- **Permanent Links:** Links remain active as long as the file exists in the storage channel.
|
|
|
|
| 64 |
- **Channel and Group Support:** Works in private chats, groups, and channels.
|
| 65 |
- **MongoDB Integration:** Store user data and info with advanced database capabilities.
|
| 66 |
- **HTTP/HTTPS Streaming:** Stream media with custom player support for all devices and browsers.
|
| 67 |
+
- **Flood Wait Handling:** Centralized handling for Telegram flood waits.
|
| 68 |
|
| 69 |
### 🧩 Advanced Features
|
| 70 |
|
|
|
|
| 76 |
- **Custom Domain Support:** Use your own domain for streaming links.
|
| 77 |
- **Customizable Templates:** Personalize HTML templates for download pages.
|
| 78 |
- **Data Center Info:** Get data center information for users and files.
|
| 79 |
+
- **Auto Set Commands:** Automatically set bot commands on startup.
|
| 80 |
|
| 81 |
### ⚙️ Technical Capabilities
|
| 82 |
|
|
|
|
| 135 |
| `MULTI_BOT_TOKENS` | Additional bot tokens for load balancing | *(empty)* | `MULTI_TOKEN1=` |
|
| 136 |
| `FORCE_CHANNEL_ID` | Channel ID users must join | *(empty)* | `-1001234567890` |
|
| 137 |
| `BANNED_CHANNELS` | Space-separated banned channel IDs | *(empty)* | `-1001234567890 -100987654321`|
|
| 138 |
+
| `SLEEP_THRESHOLD` | Threshold for client switching | `300` | `600` |
|
| 139 |
| `WORKERS` | Number of async workers | `8` | `200` |
|
| 140 |
| `NAME` | Bot application name | `ThunderF2L` | `MyFileBot` |
|
| 141 |
| `BIND_ADDRESS` | Address to bind web server | `0.0.0.0` | `127.0.0.1` |
|
|
|
|
| 146 |
| `SHORTEN_MEDIA_LINKS`| Enable URL shortening for media links | `False` | `True` |
|
| 147 |
| `URL_SHORTENER_API_KEY` | API key for URL shortening service | `""` | `"abc123def456"` |
|
| 148 |
| `URL_SHORTENER_SITE` | URL shortening service to use | `""` | `"example.com"` |
|
| 149 |
+
| `SET_COMMANDS` | Automatically set bot commands on startup | `True` | `False` |
|
| 150 |
|
| 151 |
> ℹ️ For all options, see `config_sample.env`.
|
| 152 |
|
|
|
|
| 230 |
|
| 231 |
### Commands for @BotFather
|
| 232 |
|
| 233 |
+
> If `SET_COMMANDS` is set to `True` in your configuration, the bot will automatically configure these commands.
|
| 234 |
+
|
| 235 |
+
Paste the following into the BotFather "Edit Commands" section for your bot.
|
| 236 |
|
| 237 |
```text
|
| 238 |
start - Start the bot and get a welcome message
|
| 239 |
+
link - (Group) Generate a direct link for a file or batch
|
| 240 |
+
dc - Retrieve the data center (DC) information of a user or file
|
| 241 |
+
ping - Check the bot's status and response time
|
| 242 |
about - Get information about the bot
|
| 243 |
help - Show help and usage instructions
|
| 244 |
+
status - (Admin) View bot details and current workload
|
| 245 |
+
stats - (Admin) View usage statistics and resource consumption
|
| 246 |
broadcast - (Admin) Send a message to all users
|
|
|
|
| 247 |
ban - (Admin) Ban a user
|
| 248 |
unban - (Admin) Unban a user
|
| 249 |
log - (Admin) Send bot logs
|
| 250 |
restart - (Admin) Restart the bot
|
| 251 |
shell - (Admin) Execute a shell command
|
| 252 |
+
users - (Admin) Show the total number of users
|
| 253 |
authorize - (Admin) Grant permanent access to a user
|
| 254 |
deauthorize - (Admin) Remove permanent access from a user
|
| 255 |
listauth - (Admin) List all authorized users
|
Thunder/__init__.py
CHANGED
|
@@ -3,4 +3,4 @@
|
|
| 3 |
import time
|
| 4 |
|
| 5 |
StartTime = time.time()
|
| 6 |
-
__version__ = "1.9.
|
|
|
|
| 3 |
import time
|
| 4 |
|
| 5 |
StartTime = time.time()
|
| 6 |
+
__version__ = "1.9.2"
|
Thunder/__main__.py
CHANGED
|
@@ -1,27 +1,27 @@
|
|
| 1 |
# Thunder/__main__.py
|
| 2 |
|
| 3 |
-
import os
|
| 4 |
-
import sys
|
| 5 |
-
import glob
|
| 6 |
import asyncio
|
|
|
|
| 7 |
import importlib.util
|
| 8 |
-
|
| 9 |
-
|
| 10 |
from datetime import datetime
|
| 11 |
from pathlib import Path
|
| 12 |
|
|
|
|
| 13 |
from pyrogram import idle
|
| 14 |
-
from pyrogram.errors import FloodWait
|
| 15 |
|
| 16 |
from Thunder import __version__
|
| 17 |
from Thunder.bot import StreamBot
|
| 18 |
-
from Thunder.bot.clients import
|
|
|
|
|
|
|
| 19 |
from Thunder.utils.database import db
|
|
|
|
| 20 |
from Thunder.utils.keepalive import ping_server
|
| 21 |
from Thunder.utils.logger import logger
|
| 22 |
from Thunder.utils.messages import MSG_ADMIN_RESTART_DONE
|
| 23 |
from Thunder.utils.tokens import cleanup_expired_tokens
|
| 24 |
-
from Thunder.server import web_server
|
| 25 |
from Thunder.vars import Var
|
| 26 |
|
| 27 |
PLUGIN_PATH = "Thunder/bot/plugins/*.py"
|
|
@@ -88,33 +88,23 @@ async def start_services():
|
|
| 88 |
|
| 89 |
print(" ▶ Starting Telegram Bot initialization...")
|
| 90 |
try:
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
bot_info = await StreamBot.get_me()
|
| 94 |
-
except FloodWait as e:
|
| 95 |
-
await asyncio.sleep(e.value)
|
| 96 |
-
await StreamBot.start()
|
| 97 |
-
bot_info = await StreamBot.get_me()
|
| 98 |
StreamBot.username = bot_info.username
|
| 99 |
print(f" ✓ Bot initialized successfully as @{StreamBot.username}")
|
| 100 |
|
|
|
|
|
|
|
|
|
|
| 101 |
restart_message_data = await db.get_restart_message()
|
| 102 |
if restart_message_data:
|
| 103 |
try:
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
except FloodWait as e:
|
| 111 |
-
logger.debug(f"FloodWait in restart message: sleeping for {e.value}s")
|
| 112 |
-
await asyncio.sleep(e.value)
|
| 113 |
-
await StreamBot.edit_message_text(
|
| 114 |
-
chat_id=restart_message_data["chat_id"],
|
| 115 |
-
message_id=restart_message_data["message_id"],
|
| 116 |
-
text=MSG_ADMIN_RESTART_DONE
|
| 117 |
-
)
|
| 118 |
await db.delete_restart_message(restart_message_data["message_id"])
|
| 119 |
except Exception as e:
|
| 120 |
logger.error(f"Error processing restart message: {e}", exc_info=True)
|
|
|
|
| 1 |
# Thunder/__main__.py
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
import asyncio
|
| 4 |
+
import glob
|
| 5 |
import importlib.util
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
from datetime import datetime
|
| 9 |
from pathlib import Path
|
| 10 |
|
| 11 |
+
from aiohttp import web
|
| 12 |
from pyrogram import idle
|
|
|
|
| 13 |
|
| 14 |
from Thunder import __version__
|
| 15 |
from Thunder.bot import StreamBot
|
| 16 |
+
from Thunder.bot.clients import cleanup_clients, initialize_clients
|
| 17 |
+
from Thunder.server import web_server
|
| 18 |
+
from Thunder.utils.commands import set_commands
|
| 19 |
from Thunder.utils.database import db
|
| 20 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 21 |
from Thunder.utils.keepalive import ping_server
|
| 22 |
from Thunder.utils.logger import logger
|
| 23 |
from Thunder.utils.messages import MSG_ADMIN_RESTART_DONE
|
| 24 |
from Thunder.utils.tokens import cleanup_expired_tokens
|
|
|
|
| 25 |
from Thunder.vars import Var
|
| 26 |
|
| 27 |
PLUGIN_PATH = "Thunder/bot/plugins/*.py"
|
|
|
|
| 88 |
|
| 89 |
print(" ▶ Starting Telegram Bot initialization...")
|
| 90 |
try:
|
| 91 |
+
await handle_flood_wait(StreamBot.start)
|
| 92 |
+
bot_info = await handle_flood_wait(StreamBot.get_me)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
StreamBot.username = bot_info.username
|
| 94 |
print(f" ✓ Bot initialized successfully as @{StreamBot.username}")
|
| 95 |
|
| 96 |
+
await set_commands()
|
| 97 |
+
print(" ✓ Bot commands set successfully.")
|
| 98 |
+
|
| 99 |
restart_message_data = await db.get_restart_message()
|
| 100 |
if restart_message_data:
|
| 101 |
try:
|
| 102 |
+
await handle_flood_wait(
|
| 103 |
+
StreamBot.edit_message_text,
|
| 104 |
+
chat_id=restart_message_data["chat_id"],
|
| 105 |
+
message_id=restart_message_data["message_id"],
|
| 106 |
+
text=MSG_ADMIN_RESTART_DONE
|
| 107 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
await db.delete_restart_message(restart_message_data["message_id"])
|
| 109 |
except Exception as e:
|
| 110 |
logger.error(f"Error processing restart message: {e}", exc_info=True)
|
Thunder/bot/clients.py
CHANGED
|
@@ -3,18 +3,17 @@
|
|
| 3 |
import asyncio
|
| 4 |
|
| 5 |
from pyrogram import Client
|
| 6 |
-
from pyrogram.errors import FloodWait
|
| 7 |
|
| 8 |
-
from Thunder.
|
| 9 |
from Thunder.utils.config_parser import TokenParser
|
| 10 |
-
from Thunder.
|
| 11 |
from Thunder.utils.logger import logger
|
| 12 |
-
|
| 13 |
|
| 14 |
async def cleanup_clients():
|
| 15 |
for client in multi_clients.values():
|
| 16 |
try:
|
| 17 |
-
await client.stop
|
| 18 |
except Exception as e:
|
| 19 |
logger.error(f"Error stopping client: {e}", exc_info=True)
|
| 20 |
|
|
@@ -41,18 +40,15 @@ async def initialize_clients():
|
|
| 41 |
if client_id == len(all_tokens):
|
| 42 |
await asyncio.sleep(2)
|
| 43 |
client = Client(
|
| 44 |
-
name=str(client_id),
|
| 45 |
-
api_id=Var.API_ID,
|
| 46 |
api_hash=Var.API_HASH,
|
|
|
|
| 47 |
bot_token=token,
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
| 50 |
)
|
| 51 |
-
|
| 52 |
-
await client.start()
|
| 53 |
-
except FloodWait as e:
|
| 54 |
-
await asyncio.sleep(e.value)
|
| 55 |
-
await client.start()
|
| 56 |
work_loads[client_id] = 0
|
| 57 |
print(f" ◎ Client ID {client_id} started")
|
| 58 |
return client_id, client
|
|
|
|
| 3 |
import asyncio
|
| 4 |
|
| 5 |
from pyrogram import Client
|
|
|
|
| 6 |
|
| 7 |
+
from Thunder.bot import StreamBot, multi_clients, work_loads
|
| 8 |
from Thunder.utils.config_parser import TokenParser
|
| 9 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 10 |
from Thunder.utils.logger import logger
|
| 11 |
+
from Thunder.vars import Var
|
| 12 |
|
| 13 |
async def cleanup_clients():
|
| 14 |
for client in multi_clients.values():
|
| 15 |
try:
|
| 16 |
+
await handle_flood_wait(client.stop)
|
| 17 |
except Exception as e:
|
| 18 |
logger.error(f"Error stopping client: {e}", exc_info=True)
|
| 19 |
|
|
|
|
| 40 |
if client_id == len(all_tokens):
|
| 41 |
await asyncio.sleep(2)
|
| 42 |
client = Client(
|
|
|
|
|
|
|
| 43 |
api_hash=Var.API_HASH,
|
| 44 |
+
api_id=Var.API_ID,
|
| 45 |
bot_token=token,
|
| 46 |
+
in_memory=True,
|
| 47 |
+
name=str(client_id),
|
| 48 |
+
no_updates=True,
|
| 49 |
+
sleep_threshold=Var.SLEEP_THRESHOLD
|
| 50 |
)
|
| 51 |
+
await handle_flood_wait(client.start)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
work_loads[client_id] = 0
|
| 53 |
print(f" ◎ Client ID {client_id} started")
|
| 54 |
return client_id, client
|
Thunder/bot/plugins/admin.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import html
|
| 5 |
-
import psutil
|
| 6 |
import os
|
| 7 |
import shutil
|
| 8 |
import sys
|
|
@@ -10,17 +9,18 @@ import time
|
|
| 10 |
from datetime import datetime
|
| 11 |
from io import BytesIO
|
| 12 |
|
|
|
|
| 13 |
from pyrogram import filters
|
| 14 |
from pyrogram.client import Client
|
| 15 |
from pyrogram.enums import ParseMode
|
| 16 |
-
from pyrogram.
|
| 17 |
-
from pyrogram.types import Message, InlineKeyboardButton, InlineKeyboardMarkup
|
| 18 |
|
| 19 |
from Thunder import StartTime, __version__
|
| 20 |
from Thunder.bot import StreamBot, multi_clients, work_loads
|
| 21 |
-
from Thunder.
|
| 22 |
from Thunder.utils.broadcast import broadcast_message
|
| 23 |
from Thunder.utils.database import db
|
|
|
|
| 24 |
from Thunder.utils.human_readable import humanbytes
|
| 25 |
from Thunder.utils.logger import LOG_FILE, logger
|
| 26 |
from Thunder.utils.messages import *
|
|
@@ -34,13 +34,13 @@ owner_filter = filters.private & filters.user(Var.OWNER_ID)
|
|
| 34 |
async def get_total_users(client: Client, message: Message):
|
| 35 |
try:
|
| 36 |
total = await db.total_users_count()
|
| 37 |
-
await
|
| 38 |
text=MSG_DB_STATS.format(total_users=total),
|
| 39 |
parse_mode=ParseMode.MARKDOWN,
|
| 40 |
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]))
|
| 41 |
except Exception as e:
|
| 42 |
logger.error(f"Error in get_total_users: {e}", exc_info=True)
|
| 43 |
-
await
|
| 44 |
|
| 45 |
@StreamBot.on_message(filters.command("broadcast") & owner_filter & filters.reply)
|
| 46 |
async def broadcast_handler(client: Client, message: Message):
|
|
@@ -57,13 +57,13 @@ async def show_status(client: Client, message: Message):
|
|
| 57 |
|
| 58 |
total_workload = sum(work_loads.values())
|
| 59 |
status_text_str = MSG_SYSTEM_STATUS.format(uptime=uptime_str, active_bots=len(multi_clients), total_workload=total_workload, workload_items=workload_items, version=__version__)
|
| 60 |
-
await
|
| 61 |
text=status_text_str,
|
| 62 |
parse_mode=ParseMode.MARKDOWN,
|
| 63 |
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]))
|
| 64 |
except Exception as e:
|
| 65 |
logger.error(f"Error in show_status: {e}", exc_info=True)
|
| 66 |
-
await
|
| 67 |
|
| 68 |
@StreamBot.on_message(filters.command("stats") & owner_filter)
|
| 69 |
async def show_stats(client: Client, message: Message):
|
|
@@ -99,72 +99,69 @@ async def show_stats(client: Client, message: Message):
|
|
| 99 |
download=humanbytes(net_io_counters.bytes_recv)
|
| 100 |
)
|
| 101 |
|
| 102 |
-
await
|
| 103 |
text=stats_text_val,
|
| 104 |
parse_mode=ParseMode.MARKDOWN,
|
| 105 |
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]))
|
| 106 |
except Exception as e:
|
| 107 |
logger.error(f"Error in show_stats: {e}", exc_info=True)
|
| 108 |
-
await
|
| 109 |
|
| 110 |
@StreamBot.on_message(filters.command("restart") & owner_filter)
|
| 111 |
async def restart_bot(client: Client, message: Message):
|
| 112 |
-
msg = await
|
| 113 |
await db.add_restart_message(msg.id, message.chat.id)
|
| 114 |
os.execv(sys.executable, [sys.executable, "-m", "Thunder"])
|
| 115 |
|
| 116 |
@StreamBot.on_message(filters.command("log") & owner_filter)
|
| 117 |
async def send_logs(client: Client, message: Message):
|
| 118 |
if not os.path.exists(LOG_FILE) or os.path.getsize(LOG_FILE) == 0:
|
| 119 |
-
await
|
| 120 |
return
|
| 121 |
|
| 122 |
try:
|
| 123 |
-
await message.reply_document
|
| 124 |
-
except FloodWait as e:
|
| 125 |
-
await asyncio.sleep(e.value)
|
| 126 |
-
await message.reply_document(LOG_FILE, caption=MSG_LOG_FILE_CAPTION)
|
| 127 |
except Exception as e:
|
| 128 |
logger.error(f"Error sending log file: {e}", exc_info=True)
|
| 129 |
-
await
|
| 130 |
|
| 131 |
@StreamBot.on_message(filters.command("authorize") & owner_filter)
|
| 132 |
async def authorize_command(client: Client, message: Message):
|
| 133 |
if len(message.command) != 2:
|
| 134 |
-
return await
|
| 135 |
|
| 136 |
try:
|
| 137 |
user_id = int(message.command[1])
|
| 138 |
success = await authorize(user_id, message.from_user.id)
|
| 139 |
-
await
|
| 140 |
text=MSG_AUTHORIZE_SUCCESS.format(user_id=user_id) if success else MSG_AUTHORIZE_FAILED.format(user_id=user_id))
|
| 141 |
except ValueError:
|
| 142 |
-
await
|
| 143 |
except Exception as e:
|
| 144 |
logger.error(f"Error in authorize_command: {e}", exc_info=True)
|
| 145 |
-
await
|
| 146 |
|
| 147 |
@StreamBot.on_message(filters.command("deauthorize") & owner_filter)
|
| 148 |
async def deauthorize_command(client: Client, message: Message):
|
| 149 |
if len(message.command) != 2:
|
| 150 |
-
return await
|
| 151 |
|
| 152 |
try:
|
| 153 |
user_id = int(message.command[1])
|
| 154 |
success = await deauthorize(user_id)
|
| 155 |
-
await
|
| 156 |
text=MSG_DEAUTHORIZE_SUCCESS.format(user_id=user_id) if success else MSG_DEAUTHORIZE_FAILED.format(user_id=user_id))
|
| 157 |
except ValueError:
|
| 158 |
-
await
|
| 159 |
except Exception as e:
|
| 160 |
logger.error(f"Error in deauthorize_command: {e}", exc_info=True)
|
| 161 |
-
await
|
| 162 |
|
| 163 |
@StreamBot.on_message(filters.command("listauth") & owner_filter)
|
| 164 |
async def list_authorized_command(client: Client, message: Message):
|
| 165 |
users = await list_allowed()
|
| 166 |
if not users:
|
| 167 |
-
return await
|
| 168 |
|
| 169 |
text = MSG_ADMIN_AUTH_LIST_HEADER
|
| 170 |
for i, user in enumerate(users, 1):
|
|
@@ -174,7 +171,7 @@ async def list_authorized_command(client: Client, message: Message):
|
|
| 174 |
auth_time=user['authorized_at']
|
| 175 |
)
|
| 176 |
|
| 177 |
-
await
|
| 178 |
text=text,
|
| 179 |
parse_mode=ParseMode.MARKDOWN,
|
| 180 |
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]))
|
|
@@ -182,12 +179,12 @@ async def list_authorized_command(client: Client, message: Message):
|
|
| 182 |
@StreamBot.on_message(filters.command("ban") & owner_filter)
|
| 183 |
async def ban_user_command(client: Client, message: Message):
|
| 184 |
if len(message.command) < 2:
|
| 185 |
-
return await
|
| 186 |
|
| 187 |
try:
|
| 188 |
user_id = int(message.command[1])
|
| 189 |
if user_id == Var.OWNER_ID:
|
| 190 |
-
return await
|
| 191 |
|
| 192 |
reason = " ".join(message.command[2:]) or MSG_ADMIN_NO_BAN_REASON
|
| 193 |
await db.add_banned_user(
|
|
@@ -201,47 +198,41 @@ async def ban_user_command(client: Client, message: Message):
|
|
| 201 |
if reason != MSG_ADMIN_NO_BAN_REASON:
|
| 202 |
text += MSG_BAN_REASON_SUFFIX.format(reason=reason)
|
| 203 |
|
| 204 |
-
await
|
| 205 |
|
| 206 |
try:
|
| 207 |
-
await client.send_message
|
| 208 |
-
except FloodWait as e:
|
| 209 |
-
await asyncio.sleep(e.value)
|
| 210 |
-
await client.send_message(user_id, MSG_USER_BANNED_NOTIFICATION)
|
| 211 |
except Exception as e:
|
| 212 |
logger.warning(f"Could not notify banned user {user_id}: {e}", exc_info=True)
|
| 213 |
|
| 214 |
except ValueError:
|
| 215 |
-
await
|
| 216 |
|
| 217 |
@StreamBot.on_message(filters.command("unban") & owner_filter)
|
| 218 |
async def unban_user_command(client: Client, message: Message):
|
| 219 |
if len(message.command) != 2:
|
| 220 |
-
return await
|
| 221 |
|
| 222 |
try:
|
| 223 |
user_id = int(message.command[1])
|
| 224 |
if await db.remove_banned_user(user_id=user_id):
|
| 225 |
-
await
|
| 226 |
try:
|
| 227 |
-
await client.send_message
|
| 228 |
-
except FloodWait as e:
|
| 229 |
-
await asyncio.sleep(e.value)
|
| 230 |
-
await client.send_message(user_id, MSG_USER_UNBANNED_NOTIFICATION)
|
| 231 |
except Exception as e:
|
| 232 |
logger.warning(f"Could not notify unbanned user {user_id}: {e}", exc_info=True)
|
| 233 |
else:
|
| 234 |
-
await
|
| 235 |
except ValueError:
|
| 236 |
-
await
|
| 237 |
|
| 238 |
@StreamBot.on_message(filters.command("shell") & owner_filter)
|
| 239 |
async def run_shell_command(client: Client, message: Message):
|
| 240 |
if len(message.command) < 2:
|
| 241 |
-
return await
|
| 242 |
|
| 243 |
command = " ".join(message.command[1:])
|
| 244 |
-
status_msg = await
|
| 245 |
text=MSG_SHELL_EXECUTING.format(command=html.escape(command)),
|
| 246 |
parse_mode=ParseMode.HTML)
|
| 247 |
|
|
@@ -262,26 +253,14 @@ async def run_shell_command(client: Client, message: Message):
|
|
| 262 |
|
| 263 |
output = output.strip() or MSG_SHELL_NO_OUTPUT
|
| 264 |
|
| 265 |
-
|
| 266 |
-
await status_msg.delete()
|
| 267 |
-
except FloodWait as e:
|
| 268 |
-
await asyncio.sleep(e.value)
|
| 269 |
-
await status_msg.delete()
|
| 270 |
|
| 271 |
if len(output) > 4096:
|
| 272 |
file = BytesIO(output.encode())
|
| 273 |
file.name = "shell_output.txt"
|
| 274 |
-
|
| 275 |
-
await message.reply_document(file, caption=MSG_SHELL_OUTPUT.format(command=html.escape(command)))
|
| 276 |
-
except FloodWait as e:
|
| 277 |
-
await asyncio.sleep(e.value)
|
| 278 |
-
await message.reply_document(file, caption=MSG_SHELL_OUTPUT.format(command=html.escape(command)))
|
| 279 |
else:
|
| 280 |
-
|
| 281 |
-
await retry(reply, message, text=output, parse_mode=ParseMode.HTML)
|
| 282 |
-
except FloodWait as e:
|
| 283 |
-
await asyncio.sleep(e.value)
|
| 284 |
-
await retry(reply, message, text=output, parse_mode=ParseMode.HTML)
|
| 285 |
|
| 286 |
except Exception as e:
|
| 287 |
-
await status_msg.edit_text
|
|
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import html
|
|
|
|
| 5 |
import os
|
| 6 |
import shutil
|
| 7 |
import sys
|
|
|
|
| 9 |
from datetime import datetime
|
| 10 |
from io import BytesIO
|
| 11 |
|
| 12 |
+
import psutil
|
| 13 |
from pyrogram import filters
|
| 14 |
from pyrogram.client import Client
|
| 15 |
from pyrogram.enums import ParseMode
|
| 16 |
+
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
|
|
|
|
| 17 |
|
| 18 |
from Thunder import StartTime, __version__
|
| 19 |
from Thunder.bot import StreamBot, multi_clients, work_loads
|
| 20 |
+
from Thunder.utils.bot_utils import reply
|
| 21 |
from Thunder.utils.broadcast import broadcast_message
|
| 22 |
from Thunder.utils.database import db
|
| 23 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 24 |
from Thunder.utils.human_readable import humanbytes
|
| 25 |
from Thunder.utils.logger import LOG_FILE, logger
|
| 26 |
from Thunder.utils.messages import *
|
|
|
|
| 34 |
async def get_total_users(client: Client, message: Message):
|
| 35 |
try:
|
| 36 |
total = await db.total_users_count()
|
| 37 |
+
await reply(message,
|
| 38 |
text=MSG_DB_STATS.format(total_users=total),
|
| 39 |
parse_mode=ParseMode.MARKDOWN,
|
| 40 |
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]))
|
| 41 |
except Exception as e:
|
| 42 |
logger.error(f"Error in get_total_users: {e}", exc_info=True)
|
| 43 |
+
await reply(message, text=MSG_DB_ERROR)
|
| 44 |
|
| 45 |
@StreamBot.on_message(filters.command("broadcast") & owner_filter & filters.reply)
|
| 46 |
async def broadcast_handler(client: Client, message: Message):
|
|
|
|
| 57 |
|
| 58 |
total_workload = sum(work_loads.values())
|
| 59 |
status_text_str = MSG_SYSTEM_STATUS.format(uptime=uptime_str, active_bots=len(multi_clients), total_workload=total_workload, workload_items=workload_items, version=__version__)
|
| 60 |
+
await reply(message,
|
| 61 |
text=status_text_str,
|
| 62 |
parse_mode=ParseMode.MARKDOWN,
|
| 63 |
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]))
|
| 64 |
except Exception as e:
|
| 65 |
logger.error(f"Error in show_status: {e}", exc_info=True)
|
| 66 |
+
await reply(message, text=MSG_STATUS_ERROR)
|
| 67 |
|
| 68 |
@StreamBot.on_message(filters.command("stats") & owner_filter)
|
| 69 |
async def show_stats(client: Client, message: Message):
|
|
|
|
| 99 |
download=humanbytes(net_io_counters.bytes_recv)
|
| 100 |
)
|
| 101 |
|
| 102 |
+
await reply(message,
|
| 103 |
text=stats_text_val,
|
| 104 |
parse_mode=ParseMode.MARKDOWN,
|
| 105 |
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]))
|
| 106 |
except Exception as e:
|
| 107 |
logger.error(f"Error in show_stats: {e}", exc_info=True)
|
| 108 |
+
await reply(message, text=MSG_STATUS_ERROR)
|
| 109 |
|
| 110 |
@StreamBot.on_message(filters.command("restart") & owner_filter)
|
| 111 |
async def restart_bot(client: Client, message: Message):
|
| 112 |
+
msg = await reply(message, text=MSG_RESTARTING)
|
| 113 |
await db.add_restart_message(msg.id, message.chat.id)
|
| 114 |
os.execv(sys.executable, [sys.executable, "-m", "Thunder"])
|
| 115 |
|
| 116 |
@StreamBot.on_message(filters.command("log") & owner_filter)
|
| 117 |
async def send_logs(client: Client, message: Message):
|
| 118 |
if not os.path.exists(LOG_FILE) or os.path.getsize(LOG_FILE) == 0:
|
| 119 |
+
await reply(message, text=MSG_LOG_FILE_MISSING if not os.path.exists(LOG_FILE) else MSG_LOG_FILE_EMPTY)
|
| 120 |
return
|
| 121 |
|
| 122 |
try:
|
| 123 |
+
await handle_flood_wait(message.reply_document, LOG_FILE, caption=MSG_LOG_FILE_CAPTION)
|
|
|
|
|
|
|
|
|
|
| 124 |
except Exception as e:
|
| 125 |
logger.error(f"Error sending log file: {e}", exc_info=True)
|
| 126 |
+
await reply(message, text=MSG_ERROR_GENERIC)
|
| 127 |
|
| 128 |
@StreamBot.on_message(filters.command("authorize") & owner_filter)
|
| 129 |
async def authorize_command(client: Client, message: Message):
|
| 130 |
if len(message.command) != 2:
|
| 131 |
+
return await reply(message, text=MSG_AUTHORIZE_USAGE, parse_mode=ParseMode.MARKDOWN)
|
| 132 |
|
| 133 |
try:
|
| 134 |
user_id = int(message.command[1])
|
| 135 |
success = await authorize(user_id, message.from_user.id)
|
| 136 |
+
await reply(message,
|
| 137 |
text=MSG_AUTHORIZE_SUCCESS.format(user_id=user_id) if success else MSG_AUTHORIZE_FAILED.format(user_id=user_id))
|
| 138 |
except ValueError:
|
| 139 |
+
await reply(message, text=MSG_INVALID_USER_ID)
|
| 140 |
except Exception as e:
|
| 141 |
logger.error(f"Error in authorize_command: {e}", exc_info=True)
|
| 142 |
+
await reply(message, text=MSG_ERROR_GENERIC)
|
| 143 |
|
| 144 |
@StreamBot.on_message(filters.command("deauthorize") & owner_filter)
|
| 145 |
async def deauthorize_command(client: Client, message: Message):
|
| 146 |
if len(message.command) != 2:
|
| 147 |
+
return await reply(message, text=MSG_DEAUTHORIZE_USAGE, parse_mode=ParseMode.MARKDOWN)
|
| 148 |
|
| 149 |
try:
|
| 150 |
user_id = int(message.command[1])
|
| 151 |
success = await deauthorize(user_id)
|
| 152 |
+
await reply(message,
|
| 153 |
text=MSG_DEAUTHORIZE_SUCCESS.format(user_id=user_id) if success else MSG_DEAUTHORIZE_FAILED.format(user_id=user_id))
|
| 154 |
except ValueError:
|
| 155 |
+
await reply(message, text=MSG_INVALID_USER_ID)
|
| 156 |
except Exception as e:
|
| 157 |
logger.error(f"Error in deauthorize_command: {e}", exc_info=True)
|
| 158 |
+
await reply(message, text=MSG_ERROR_GENERIC)
|
| 159 |
|
| 160 |
@StreamBot.on_message(filters.command("listauth") & owner_filter)
|
| 161 |
async def list_authorized_command(client: Client, message: Message):
|
| 162 |
users = await list_allowed()
|
| 163 |
if not users:
|
| 164 |
+
return await reply(message, text=MSG_NO_AUTH_USERS)
|
| 165 |
|
| 166 |
text = MSG_ADMIN_AUTH_LIST_HEADER
|
| 167 |
for i, user in enumerate(users, 1):
|
|
|
|
| 171 |
auth_time=user['authorized_at']
|
| 172 |
)
|
| 173 |
|
| 174 |
+
await reply(message,
|
| 175 |
text=text,
|
| 176 |
parse_mode=ParseMode.MARKDOWN,
|
| 177 |
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]))
|
|
|
|
| 179 |
@StreamBot.on_message(filters.command("ban") & owner_filter)
|
| 180 |
async def ban_user_command(client: Client, message: Message):
|
| 181 |
if len(message.command) < 2:
|
| 182 |
+
return await reply(message, text=MSG_BAN_USAGE)
|
| 183 |
|
| 184 |
try:
|
| 185 |
user_id = int(message.command[1])
|
| 186 |
if user_id == Var.OWNER_ID:
|
| 187 |
+
return await reply(message, text=MSG_CANNOT_BAN_OWNER)
|
| 188 |
|
| 189 |
reason = " ".join(message.command[2:]) or MSG_ADMIN_NO_BAN_REASON
|
| 190 |
await db.add_banned_user(
|
|
|
|
| 198 |
if reason != MSG_ADMIN_NO_BAN_REASON:
|
| 199 |
text += MSG_BAN_REASON_SUFFIX.format(reason=reason)
|
| 200 |
|
| 201 |
+
await reply(message, text=text)
|
| 202 |
|
| 203 |
try:
|
| 204 |
+
await handle_flood_wait(client.send_message, user_id, MSG_USER_BANNED_NOTIFICATION)
|
|
|
|
|
|
|
|
|
|
| 205 |
except Exception as e:
|
| 206 |
logger.warning(f"Could not notify banned user {user_id}: {e}", exc_info=True)
|
| 207 |
|
| 208 |
except ValueError:
|
| 209 |
+
await reply(message, text=MSG_INVALID_USER_ID)
|
| 210 |
|
| 211 |
@StreamBot.on_message(filters.command("unban") & owner_filter)
|
| 212 |
async def unban_user_command(client: Client, message: Message):
|
| 213 |
if len(message.command) != 2:
|
| 214 |
+
return await reply(message, text=MSG_UNBAN_USAGE)
|
| 215 |
|
| 216 |
try:
|
| 217 |
user_id = int(message.command[1])
|
| 218 |
if await db.remove_banned_user(user_id=user_id):
|
| 219 |
+
await reply(message, text=MSG_ADMIN_USER_UNBANNED.format(user_id=user_id))
|
| 220 |
try:
|
| 221 |
+
await handle_flood_wait(client.send_message, user_id, MSG_USER_UNBANNED_NOTIFICATION)
|
|
|
|
|
|
|
|
|
|
| 222 |
except Exception as e:
|
| 223 |
logger.warning(f"Could not notify unbanned user {user_id}: {e}", exc_info=True)
|
| 224 |
else:
|
| 225 |
+
await reply(message, text=MSG_USER_NOT_IN_BAN_LIST.format(user_id=user_id))
|
| 226 |
except ValueError:
|
| 227 |
+
await reply(message, text=MSG_INVALID_USER_ID)
|
| 228 |
|
| 229 |
@StreamBot.on_message(filters.command("shell") & owner_filter)
|
| 230 |
async def run_shell_command(client: Client, message: Message):
|
| 231 |
if len(message.command) < 2:
|
| 232 |
+
return await reply(message, text=MSG_SHELL_USAGE, parse_mode=ParseMode.HTML)
|
| 233 |
|
| 234 |
command = " ".join(message.command[1:])
|
| 235 |
+
status_msg = await reply(message,
|
| 236 |
text=MSG_SHELL_EXECUTING.format(command=html.escape(command)),
|
| 237 |
parse_mode=ParseMode.HTML)
|
| 238 |
|
|
|
|
| 253 |
|
| 254 |
output = output.strip() or MSG_SHELL_NO_OUTPUT
|
| 255 |
|
| 256 |
+
await handle_flood_wait(status_msg.delete)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
if len(output) > 4096:
|
| 259 |
file = BytesIO(output.encode())
|
| 260 |
file.name = "shell_output.txt"
|
| 261 |
+
await handle_flood_wait(message.reply_document, file, caption=MSG_SHELL_OUTPUT.format(command=html.escape(command)))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
else:
|
| 263 |
+
await reply(message, text=output, parse_mode=ParseMode.HTML)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
except Exception as e:
|
| 266 |
+
await handle_flood_wait(status_msg.edit_text, MSG_SHELL_ERROR.format(error=html.escape(str(e))), parse_mode=ParseMode.HTML)
|
Thunder/bot/plugins/callbacks.py
CHANGED
|
@@ -1,37 +1,32 @@
|
|
| 1 |
# Thunder/bot/plugins/callbacks.py
|
| 2 |
|
| 3 |
-
import asyncio
|
| 4 |
from pyrogram import Client, filters
|
| 5 |
-
from pyrogram.
|
| 6 |
-
from pyrogram.
|
|
|
|
| 7 |
|
| 8 |
from Thunder.bot import StreamBot
|
| 9 |
-
from Thunder.
|
|
|
|
|
|
|
| 10 |
from Thunder.utils.logger import logger
|
| 11 |
from Thunder.utils.messages import *
|
| 12 |
-
from Thunder.
|
| 13 |
-
from Thunder.utils.broadcast import broadcast_ids
|
| 14 |
|
| 15 |
async def get_force_channel_button(client: Client):
|
| 16 |
if not Var.FORCE_CHANNEL_ID:
|
| 17 |
return None
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
chat = await client.get_chat(Var.FORCE_CHANNEL_ID)
|
| 22 |
invite_link = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
|
| 23 |
if invite_link:
|
| 24 |
return [InlineKeyboardButton(
|
| 25 |
MSG_BUTTON_JOIN_CHANNEL.format(channel_title=chat.title or "Channel"),
|
| 26 |
url=invite_link
|
| 27 |
)]
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
logger.debug(f"FloodWait: Waiting {e.value} seconds (attempt {attempt+1}/{max_retries})")
|
| 31 |
-
await asyncio.sleep(e.value)
|
| 32 |
-
except Exception as e:
|
| 33 |
-
logger.error(f"Error getting force channel button: {e}", exc_info=True)
|
| 34 |
-
break
|
| 35 |
return None
|
| 36 |
|
| 37 |
@StreamBot.on_callback_query(filters.regex(r"^help_command$"))
|
|
@@ -43,7 +38,8 @@ async def help_callback(client: Client, callback_query: CallbackQuery):
|
|
| 43 |
if force_button:
|
| 44 |
buttons.append(force_button)
|
| 45 |
buttons.append([InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")])
|
| 46 |
-
await
|
|
|
|
| 47 |
text=MSG_HELP.format(max_files=Var.MAX_BATCH_FILES),
|
| 48 |
reply_markup=InlineKeyboardMarkup(buttons),
|
| 49 |
link_preview_options=LinkPreviewOptions(is_disabled=True)
|
|
@@ -52,7 +48,7 @@ async def help_callback(client: Client, callback_query: CallbackQuery):
|
|
| 52 |
pass
|
| 53 |
except Exception as e:
|
| 54 |
logger.error(f"Error in help callback: {e}", exc_info=True)
|
| 55 |
-
await callback_query.answer
|
| 56 |
|
| 57 |
@StreamBot.on_callback_query(filters.regex(r"^about_command$"))
|
| 58 |
async def about_callback(client: Client, callback_query: CallbackQuery):
|
|
@@ -65,7 +61,8 @@ async def about_callback(client: Client, callback_query: CallbackQuery):
|
|
| 65 |
InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")
|
| 66 |
]
|
| 67 |
]
|
| 68 |
-
await
|
|
|
|
| 69 |
text=MSG_ABOUT,
|
| 70 |
reply_markup=InlineKeyboardMarkup(buttons),
|
| 71 |
link_preview_options=LinkPreviewOptions(is_disabled=True)
|
|
@@ -74,38 +71,39 @@ async def about_callback(client: Client, callback_query: CallbackQuery):
|
|
| 74 |
pass
|
| 75 |
except Exception as e:
|
| 76 |
logger.error(f"Error in about callback: {e}", exc_info=True)
|
| 77 |
-
await callback_query.answer
|
| 78 |
|
| 79 |
@StreamBot.on_callback_query(filters.regex(r"^restart_broadcast$"))
|
| 80 |
async def restart_broadcast_callback(client: Client, callback_query: CallbackQuery):
|
| 81 |
if not await owner_only(client, callback_query):
|
| 82 |
return
|
| 83 |
try:
|
| 84 |
-
await callback_query.answer
|
| 85 |
buttons = [
|
| 86 |
[
|
| 87 |
InlineKeyboardButton(MSG_BUTTON_GET_HELP, callback_data="help_command"),
|
| 88 |
InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")
|
| 89 |
]
|
| 90 |
]
|
| 91 |
-
await
|
|
|
|
| 92 |
MSG_ERROR_BROADCAST_INSTRUCTION,
|
| 93 |
reply_markup=InlineKeyboardMarkup(buttons),
|
| 94 |
link_preview_options=LinkPreviewOptions(is_disabled=True)
|
| 95 |
)
|
| 96 |
except Exception as e:
|
| 97 |
logger.error(f"Error in restart broadcast callback: {e}", exc_info=True)
|
| 98 |
-
await callback_query.answer
|
| 99 |
|
| 100 |
@StreamBot.on_callback_query(filters.regex(r"^close_panel$"))
|
| 101 |
async def close_panel_callback(client: Client, callback_query: CallbackQuery):
|
| 102 |
try:
|
| 103 |
-
await callback_query.answer
|
| 104 |
-
await callback_query.message.delete
|
| 105 |
if callback_query.message.reply_to_message:
|
| 106 |
try:
|
| 107 |
reply_msg = callback_query.message.reply_to_message
|
| 108 |
-
await reply_msg.delete
|
| 109 |
except Exception:
|
| 110 |
pass
|
| 111 |
except Exception as e:
|
|
@@ -117,21 +115,23 @@ async def cancel_broadcast(client: Client, callback_query: CallbackQuery):
|
|
| 117 |
broadcast_id = callback_query.data.split("_")[1]
|
| 118 |
if broadcast_id in broadcast_ids:
|
| 119 |
broadcast_ids[broadcast_id]["cancelled"] = True
|
| 120 |
-
await
|
|
|
|
| 121 |
MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id)
|
| 122 |
)
|
| 123 |
else:
|
| 124 |
-
await
|
|
|
|
| 125 |
MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id),
|
| 126 |
show_alert=True
|
| 127 |
)
|
| 128 |
except Exception as e:
|
| 129 |
logger.error(f"Error in cancel broadcast callback: {e}", exc_info=True)
|
| 130 |
-
await callback_query.answer
|
| 131 |
|
| 132 |
@StreamBot.on_callback_query()
|
| 133 |
async def fallback_callback(client: Client, callback_query: CallbackQuery):
|
| 134 |
try:
|
| 135 |
-
await callback_query.answer
|
| 136 |
except Exception as e:
|
| 137 |
logger.error(f"Error in fallback callback: {e}", exc_info=True)
|
|
|
|
| 1 |
# Thunder/bot/plugins/callbacks.py
|
| 2 |
|
|
|
|
| 3 |
from pyrogram import Client, filters
|
| 4 |
+
from pyrogram.errors import MessageNotModified
|
| 5 |
+
from pyrogram.types import (CallbackQuery, InlineKeyboardButton,
|
| 6 |
+
InlineKeyboardMarkup, LinkPreviewOptions)
|
| 7 |
|
| 8 |
from Thunder.bot import StreamBot
|
| 9 |
+
from Thunder.utils.broadcast import broadcast_ids
|
| 10 |
+
from Thunder.utils.decorators import owner_only
|
| 11 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 12 |
from Thunder.utils.logger import logger
|
| 13 |
from Thunder.utils.messages import *
|
| 14 |
+
from Thunder.vars import Var
|
|
|
|
| 15 |
|
| 16 |
async def get_force_channel_button(client: Client):
|
| 17 |
if not Var.FORCE_CHANNEL_ID:
|
| 18 |
return None
|
| 19 |
+
try:
|
| 20 |
+
chat = await handle_flood_wait(client.get_chat, Var.FORCE_CHANNEL_ID)
|
| 21 |
+
if chat:
|
|
|
|
| 22 |
invite_link = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
|
| 23 |
if invite_link:
|
| 24 |
return [InlineKeyboardButton(
|
| 25 |
MSG_BUTTON_JOIN_CHANNEL.format(channel_title=chat.title or "Channel"),
|
| 26 |
url=invite_link
|
| 27 |
)]
|
| 28 |
+
except Exception as e:
|
| 29 |
+
logger.error(f"Error getting force channel button: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
return None
|
| 31 |
|
| 32 |
@StreamBot.on_callback_query(filters.regex(r"^help_command$"))
|
|
|
|
| 38 |
if force_button:
|
| 39 |
buttons.append(force_button)
|
| 40 |
buttons.append([InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")])
|
| 41 |
+
await handle_flood_wait(
|
| 42 |
+
callback_query.message.edit_text,
|
| 43 |
text=MSG_HELP.format(max_files=Var.MAX_BATCH_FILES),
|
| 44 |
reply_markup=InlineKeyboardMarkup(buttons),
|
| 45 |
link_preview_options=LinkPreviewOptions(is_disabled=True)
|
|
|
|
| 48 |
pass
|
| 49 |
except Exception as e:
|
| 50 |
logger.error(f"Error in help callback: {e}", exc_info=True)
|
| 51 |
+
await handle_flood_wait(callback_query.answer, "An error occurred. Please try again.", show_alert=True)
|
| 52 |
|
| 53 |
@StreamBot.on_callback_query(filters.regex(r"^about_command$"))
|
| 54 |
async def about_callback(client: Client, callback_query: CallbackQuery):
|
|
|
|
| 61 |
InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")
|
| 62 |
]
|
| 63 |
]
|
| 64 |
+
await handle_flood_wait(
|
| 65 |
+
callback_query.message.edit_text,
|
| 66 |
text=MSG_ABOUT,
|
| 67 |
reply_markup=InlineKeyboardMarkup(buttons),
|
| 68 |
link_preview_options=LinkPreviewOptions(is_disabled=True)
|
|
|
|
| 71 |
pass
|
| 72 |
except Exception as e:
|
| 73 |
logger.error(f"Error in about callback: {e}", exc_info=True)
|
| 74 |
+
await handle_flood_wait(callback_query.answer, "An error occurred. Please try again.", show_alert=True)
|
| 75 |
|
| 76 |
@StreamBot.on_callback_query(filters.regex(r"^restart_broadcast$"))
|
| 77 |
async def restart_broadcast_callback(client: Client, callback_query: CallbackQuery):
|
| 78 |
if not await owner_only(client, callback_query):
|
| 79 |
return
|
| 80 |
try:
|
| 81 |
+
await handle_flood_wait(callback_query.answer, MSG_ERROR_BROADCAST_RESTART, show_alert=True)
|
| 82 |
buttons = [
|
| 83 |
[
|
| 84 |
InlineKeyboardButton(MSG_BUTTON_GET_HELP, callback_data="help_command"),
|
| 85 |
InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")
|
| 86 |
]
|
| 87 |
]
|
| 88 |
+
await handle_flood_wait(
|
| 89 |
+
callback_query.message.edit_text,
|
| 90 |
MSG_ERROR_BROADCAST_INSTRUCTION,
|
| 91 |
reply_markup=InlineKeyboardMarkup(buttons),
|
| 92 |
link_preview_options=LinkPreviewOptions(is_disabled=True)
|
| 93 |
)
|
| 94 |
except Exception as e:
|
| 95 |
logger.error(f"Error in restart broadcast callback: {e}", exc_info=True)
|
| 96 |
+
await handle_flood_wait(callback_query.answer, "An error occurred. Please try again.", show_alert=True)
|
| 97 |
|
| 98 |
@StreamBot.on_callback_query(filters.regex(r"^close_panel$"))
|
| 99 |
async def close_panel_callback(client: Client, callback_query: CallbackQuery):
|
| 100 |
try:
|
| 101 |
+
await handle_flood_wait(callback_query.answer)
|
| 102 |
+
await handle_flood_wait(callback_query.message.delete)
|
| 103 |
if callback_query.message.reply_to_message:
|
| 104 |
try:
|
| 105 |
reply_msg = callback_query.message.reply_to_message
|
| 106 |
+
await handle_flood_wait(reply_msg.delete)
|
| 107 |
except Exception:
|
| 108 |
pass
|
| 109 |
except Exception as e:
|
|
|
|
| 115 |
broadcast_id = callback_query.data.split("_")[1]
|
| 116 |
if broadcast_id in broadcast_ids:
|
| 117 |
broadcast_ids[broadcast_id]["cancelled"] = True
|
| 118 |
+
await handle_flood_wait(
|
| 119 |
+
callback_query.message.edit_text,
|
| 120 |
MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id)
|
| 121 |
)
|
| 122 |
else:
|
| 123 |
+
await handle_flood_wait(
|
| 124 |
+
callback_query.answer,
|
| 125 |
MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id),
|
| 126 |
show_alert=True
|
| 127 |
)
|
| 128 |
except Exception as e:
|
| 129 |
logger.error(f"Error in cancel broadcast callback: {e}", exc_info=True)
|
| 130 |
+
await handle_flood_wait(callback_query.answer, "An error occurred. Please try again.", show_alert=True)
|
| 131 |
|
| 132 |
@StreamBot.on_callback_query()
|
| 133 |
async def fallback_callback(client: Client, callback_query: CallbackQuery):
|
| 134 |
try:
|
| 135 |
+
await handle_flood_wait(callback_query.answer, MSG_ERROR_CALLBACK_UNSUPPORTED, show_alert=True)
|
| 136 |
except Exception as e:
|
| 137 |
logger.error(f"Error in fallback callback: {e}", exc_info=True)
|
Thunder/bot/plugins/common.py
CHANGED
|
@@ -1,41 +1,24 @@
|
|
| 1 |
# Thunder/bot/plugins/common.py
|
| 2 |
|
| 3 |
-
import asyncio
|
| 4 |
import time
|
| 5 |
from datetime import datetime, timedelta
|
|
|
|
| 6 |
from pyrogram import Client, filters
|
| 7 |
-
from pyrogram.
|
| 8 |
-
|
| 9 |
-
InlineKeyboardButton,
|
| 10 |
-
InlineKeyboardMarkup,
|
| 11 |
-
LinkPreviewOptions,
|
| 12 |
-
Message,
|
| 13 |
-
User,
|
| 14 |
-
)
|
| 15 |
|
| 16 |
from Thunder.bot import StreamBot
|
| 17 |
-
from Thunder.
|
|
|
|
| 18 |
from Thunder.utils.database import db
|
| 19 |
from Thunder.utils.decorators import check_banned
|
|
|
|
| 20 |
from Thunder.utils.force_channel import force_channel_check, get_force_info
|
| 21 |
-
from Thunder.utils.
|
|
|
|
| 22 |
from Thunder.utils.logger import logger
|
| 23 |
from Thunder.utils.messages import *
|
| 24 |
-
from Thunder.
|
| 25 |
-
from Thunder.utils.file_properties import get_fsize, get_fname, parse_fid
|
| 26 |
-
|
| 27 |
-
async def retry(func, *args, max_retries=2, **kwargs):
|
| 28 |
-
for attempt in range(max_retries):
|
| 29 |
-
try:
|
| 30 |
-
return await func(*args, **kwargs)
|
| 31 |
-
except FloodWait as e:
|
| 32 |
-
wait_time = float(e.value) if isinstance(e.value, (int, float)) else 10.0
|
| 33 |
-
logger.debug(f"FloodWait: Retrying in {wait_time}s (attempt {attempt+1}/{max_retries})")
|
| 34 |
-
await asyncio.sleep(wait_time)
|
| 35 |
-
return await func(*args, **kwargs)
|
| 36 |
-
|
| 37 |
-
async def reply(msg: Message, **kwargs):
|
| 38 |
-
return await msg.reply_text(**kwargs, quote=True, link_preview_options=LinkPreviewOptions(is_disabled=True))
|
| 39 |
|
| 40 |
@StreamBot.on_message(filters.command("start") & filters.private)
|
| 41 |
async def start_command(bot: Client, msg: Message):
|
|
@@ -54,13 +37,13 @@ async def start_command(bot: Client, msg: Message):
|
|
| 54 |
token = await db.token_col.find_one({"token": payload})
|
| 55 |
if token:
|
| 56 |
if token["user_id"] != user.id:
|
| 57 |
-
return await
|
| 58 |
reason="This activation link is not for your account.",
|
| 59 |
error_id=str(int(time.time()))[-8:]
|
| 60 |
))
|
| 61 |
|
| 62 |
if token.get("activated"):
|
| 63 |
-
return await
|
| 64 |
reason="Token has already been activated.",
|
| 65 |
error_id=str(int(time.time()))[-8:]
|
| 66 |
))
|
|
@@ -74,9 +57,9 @@ async def start_command(bot: Client, msg: Message):
|
|
| 74 |
)
|
| 75 |
|
| 76 |
hrs = round((exp - now).total_seconds() / 3600, 1)
|
| 77 |
-
return await
|
| 78 |
else:
|
| 79 |
-
return await
|
| 80 |
|
| 81 |
txt = MSG_WELCOME.format(user_name=user.first_name if user else "Unknown")
|
| 82 |
link, title = await get_force_info(bot)
|
|
@@ -93,7 +76,7 @@ async def start_command(bot: Client, msg: Message):
|
|
| 93 |
if link:
|
| 94 |
btns.append([InlineKeyboardButton(MSG_BUTTON_JOIN_CHANNEL.format(channel_title=title), url=link)])
|
| 95 |
|
| 96 |
-
await
|
| 97 |
|
| 98 |
@StreamBot.on_message(filters.command("help") & filters.private)
|
| 99 |
async def help_command(bot: Client, msg: Message):
|
|
@@ -110,7 +93,7 @@ async def help_command(bot: Client, msg: Message):
|
|
| 110 |
btns.append([InlineKeyboardButton(MSG_BUTTON_JOIN_CHANNEL.format(channel_title=title), url=link)])
|
| 111 |
|
| 112 |
btns.append([InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")])
|
| 113 |
-
await
|
| 114 |
|
| 115 |
@StreamBot.on_message(filters.command("about") & filters.private)
|
| 116 |
async def about_command(bot: Client, msg: Message):
|
|
@@ -125,7 +108,7 @@ async def about_command(bot: Client, msg: Message):
|
|
| 125 |
InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
|
| 126 |
]
|
| 127 |
|
| 128 |
-
await
|
| 129 |
|
| 130 |
async def send_user_dc(msg: Message, user: User):
|
| 131 |
txt = await gen_dc_txt(user)
|
|
@@ -134,7 +117,7 @@ async def send_user_dc(msg: Message, user: User):
|
|
| 134 |
[InlineKeyboardButton(MSG_BUTTON_VIEW_PROFILE, url=url)],
|
| 135 |
[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
|
| 136 |
]
|
| 137 |
-
await
|
| 138 |
|
| 139 |
async def send_file_dc(msg: Message, file_msg: Message):
|
| 140 |
try:
|
|
@@ -168,7 +151,7 @@ async def send_file_dc(msg: Message, file_msg: Message):
|
|
| 168 |
)
|
| 169 |
|
| 170 |
btns = [[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]
|
| 171 |
-
await
|
| 172 |
|
| 173 |
except Exception as e:
|
| 174 |
logger.error(f"File DC error: {e}", exc_info=True)
|
|
@@ -214,7 +197,7 @@ async def ping_command(bot: Client, msg: Message):
|
|
| 214 |
if not await force_channel_check(bot, msg):
|
| 215 |
return
|
| 216 |
start = time.time()
|
| 217 |
-
sent = await
|
| 218 |
end = time.time()
|
| 219 |
ms = (end - start) * 1000
|
| 220 |
|
|
@@ -223,7 +206,7 @@ async def ping_command(bot: Client, msg: Message):
|
|
| 223 |
InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
|
| 224 |
]
|
| 225 |
|
| 226 |
-
await
|
| 227 |
MSG_PING_RESPONSE.format(time_taken_ms=ms),
|
| 228 |
reply_markup=InlineKeyboardMarkup(btns),
|
| 229 |
link_preview_options=LinkPreviewOptions(is_disabled=True)
|
|
|
|
| 1 |
# Thunder/bot/plugins/common.py
|
| 2 |
|
|
|
|
| 3 |
import time
|
| 4 |
from datetime import datetime, timedelta
|
| 5 |
+
|
| 6 |
from pyrogram import Client, filters
|
| 7 |
+
from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
|
| 8 |
+
LinkPreviewOptions, Message, User)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
from Thunder.bot import StreamBot
|
| 11 |
+
from Thunder.utils.bot_utils import (gen_dc_txt, get_user, log_newusr, reply,
|
| 12 |
+
reply_user_err)
|
| 13 |
from Thunder.utils.database import db
|
| 14 |
from Thunder.utils.decorators import check_banned
|
| 15 |
+
from Thunder.utils.file_properties import get_fname, get_fsize, parse_fid
|
| 16 |
from Thunder.utils.force_channel import force_channel_check, get_force_info
|
| 17 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 18 |
+
from Thunder.utils.human_readable import humanbytes
|
| 19 |
from Thunder.utils.logger import logger
|
| 20 |
from Thunder.utils.messages import *
|
| 21 |
+
from Thunder.vars import Var
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
@StreamBot.on_message(filters.command("start") & filters.private)
|
| 24 |
async def start_command(bot: Client, msg: Message):
|
|
|
|
| 37 |
token = await db.token_col.find_one({"token": payload})
|
| 38 |
if token:
|
| 39 |
if token["user_id"] != user.id:
|
| 40 |
+
return await handle_flood_wait(msg.reply_text, text=MSG_TOKEN_FAILED.format(
|
| 41 |
reason="This activation link is not for your account.",
|
| 42 |
error_id=str(int(time.time()))[-8:]
|
| 43 |
))
|
| 44 |
|
| 45 |
if token.get("activated"):
|
| 46 |
+
return await handle_flood_wait(msg.reply_text, text=MSG_TOKEN_FAILED.format(
|
| 47 |
reason="Token has already been activated.",
|
| 48 |
error_id=str(int(time.time()))[-8:]
|
| 49 |
))
|
|
|
|
| 57 |
)
|
| 58 |
|
| 59 |
hrs = round((exp - now).total_seconds() / 3600, 1)
|
| 60 |
+
return await handle_flood_wait(msg.reply_text, text=MSG_TOKEN_ACTIVATED.format(duration_hours=hrs))
|
| 61 |
else:
|
| 62 |
+
return await handle_flood_wait(msg.reply_text, text=MSG_TOKEN_INVALID)
|
| 63 |
|
| 64 |
txt = MSG_WELCOME.format(user_name=user.first_name if user else "Unknown")
|
| 65 |
link, title = await get_force_info(bot)
|
|
|
|
| 76 |
if link:
|
| 77 |
btns.append([InlineKeyboardButton(MSG_BUTTON_JOIN_CHANNEL.format(channel_title=title), url=link)])
|
| 78 |
|
| 79 |
+
await handle_flood_wait(msg.reply_text, text=txt, reply_markup=InlineKeyboardMarkup(btns))
|
| 80 |
|
| 81 |
@StreamBot.on_message(filters.command("help") & filters.private)
|
| 82 |
async def help_command(bot: Client, msg: Message):
|
|
|
|
| 93 |
btns.append([InlineKeyboardButton(MSG_BUTTON_JOIN_CHANNEL.format(channel_title=title), url=link)])
|
| 94 |
|
| 95 |
btns.append([InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")])
|
| 96 |
+
await handle_flood_wait(msg.reply_text, text=txt, reply_markup=InlineKeyboardMarkup(btns))
|
| 97 |
|
| 98 |
@StreamBot.on_message(filters.command("about") & filters.private)
|
| 99 |
async def about_command(bot: Client, msg: Message):
|
|
|
|
| 108 |
InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
|
| 109 |
]
|
| 110 |
|
| 111 |
+
await handle_flood_wait(msg.reply_text, text=MSG_ABOUT, reply_markup=InlineKeyboardMarkup(btns))
|
| 112 |
|
| 113 |
async def send_user_dc(msg: Message, user: User):
|
| 114 |
txt = await gen_dc_txt(user)
|
|
|
|
| 117 |
[InlineKeyboardButton(MSG_BUTTON_VIEW_PROFILE, url=url)],
|
| 118 |
[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
|
| 119 |
]
|
| 120 |
+
await handle_flood_wait(msg.reply_text, text=txt, reply_markup=InlineKeyboardMarkup(btns))
|
| 121 |
|
| 122 |
async def send_file_dc(msg: Message, file_msg: Message):
|
| 123 |
try:
|
|
|
|
| 151 |
)
|
| 152 |
|
| 153 |
btns = [[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]
|
| 154 |
+
await handle_flood_wait(msg.reply_text, text=txt, reply_markup=InlineKeyboardMarkup(btns))
|
| 155 |
|
| 156 |
except Exception as e:
|
| 157 |
logger.error(f"File DC error: {e}", exc_info=True)
|
|
|
|
| 197 |
if not await force_channel_check(bot, msg):
|
| 198 |
return
|
| 199 |
start = time.time()
|
| 200 |
+
sent = await handle_flood_wait(msg.reply_text, text=MSG_PING_START)
|
| 201 |
end = time.time()
|
| 202 |
ms = (end - start) * 1000
|
| 203 |
|
|
|
|
| 206 |
InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
|
| 207 |
]
|
| 208 |
|
| 209 |
+
await handle_flood_wait(sent.edit_text,
|
| 210 |
MSG_PING_RESPONSE.format(time_taken_ms=ms),
|
| 211 |
reply_markup=InlineKeyboardMarkup(btns),
|
| 212 |
link_preview_options=LinkPreviewOptions(is_disabled=True)
|
Thunder/bot/plugins/stream.py
CHANGED
|
@@ -1,42 +1,33 @@
|
|
| 1 |
# Thunder/bot/plugins/stream.py
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
-
from typing import
|
| 5 |
-
|
| 6 |
-
from pyrogram
|
| 7 |
-
from pyrogram.
|
|
|
|
|
|
|
|
|
|
| 8 |
from Thunder.bot import StreamBot
|
|
|
|
|
|
|
| 9 |
from Thunder.utils.database import db
|
| 10 |
-
from Thunder.utils.
|
|
|
|
|
|
|
|
|
|
| 11 |
from Thunder.utils.logger import logger
|
|
|
|
| 12 |
from Thunder.vars import Var
|
| 13 |
-
|
| 14 |
-
from Thunder.utils.force_channel import force_channel_check
|
| 15 |
-
from Thunder.utils.bot_utils import notify_own, reply_user_err, log_newusr, gen_links, is_admin, handle_flood_wait
|
| 16 |
|
| 17 |
async def fwd_media(m_msg: Message) -> Optional[Message]:
|
| 18 |
try:
|
| 19 |
-
return await m_msg.copy
|
| 20 |
-
except
|
| 21 |
-
logger.debug(f"FloodWait: fwd_media copy, sleep {e.value}s")
|
| 22 |
-
await asyncio.sleep(e.value + 1)
|
| 23 |
-
try:
|
| 24 |
-
return await m_msg.copy(chat_id=Var.BIN_CHANNEL)
|
| 25 |
-
except Exception as retry_e:
|
| 26 |
-
logger.error(f"Error fwd_media copy on retry after FloodWait: {retry_e}", exc_info=True)
|
| 27 |
-
return None
|
| 28 |
-
except RPCError as e:
|
| 29 |
if "MEDIA_CAPTION_TOO_LONG" in str(e):
|
| 30 |
logger.debug(f"MEDIA_CAPTION_TOO_LONG error, retrying without caption: {e}")
|
| 31 |
-
|
| 32 |
-
return await m_msg.copy(chat_id=Var.BIN_CHANNEL, caption=None)
|
| 33 |
-
except Exception as retry_e:
|
| 34 |
-
logger.error(f"Error fwd_media copy on retry without caption: {retry_e}", exc_info=True)
|
| 35 |
-
return None
|
| 36 |
-
else:
|
| 37 |
-
logger.error(f"Error fwd_media copy: {e}", exc_info=True)
|
| 38 |
-
return None
|
| 39 |
-
except Exception as e:
|
| 40 |
logger.error(f"Error fwd_media copy: {e}", exc_info=True)
|
| 41 |
return None
|
| 42 |
|
|
@@ -232,7 +223,7 @@ async def process_single(bot: Client, msg: Message, file_msg: Message, status_ms
|
|
| 232 |
return links
|
| 233 |
except Exception as e:
|
| 234 |
if status_msg:
|
| 235 |
-
await status_msg.edit_text
|
| 236 |
if str(e):
|
| 237 |
await notify_own(bot, MSG_CRITICAL_ERROR.format(
|
| 238 |
error=str(e),
|
|
@@ -259,11 +250,9 @@ async def process_batch(bot: Client, msg: Message, start_id: int, count: int, st
|
|
| 259 |
except MessageNotModified:
|
| 260 |
pass
|
| 261 |
try:
|
| 262 |
-
messages = await bot.get_messages
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
await asyncio.sleep(e.value + 1)
|
| 266 |
-
messages = await bot.get_messages(msg.chat.id, batch_ids)
|
| 267 |
except Exception as e:
|
| 268 |
logger.error(f"Error getting messages in batch: {e}", exc_info=True)
|
| 269 |
messages = []
|
|
|
|
| 1 |
# Thunder/bot/plugins/stream.py
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
+
from typing import Any, Dict, Optional
|
| 5 |
+
|
| 6 |
+
from pyrogram import Client, enums, filters
|
| 7 |
+
from pyrogram.errors import MessageNotModified
|
| 8 |
+
from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
|
| 9 |
+
LinkPreviewOptions, Message)
|
| 10 |
+
|
| 11 |
from Thunder.bot import StreamBot
|
| 12 |
+
from Thunder.utils.bot_utils import (gen_links, is_admin, log_newusr, notify_own,
|
| 13 |
+
reply_user_err)
|
| 14 |
from Thunder.utils.database import db
|
| 15 |
+
from Thunder.utils.decorators import (check_banned, get_shortener_status,
|
| 16 |
+
require_token)
|
| 17 |
+
from Thunder.utils.force_channel import force_channel_check
|
| 18 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 19 |
from Thunder.utils.logger import logger
|
| 20 |
+
from Thunder.utils.messages import *
|
| 21 |
from Thunder.vars import Var
|
| 22 |
+
|
|
|
|
|
|
|
| 23 |
|
| 24 |
async def fwd_media(m_msg: Message) -> Optional[Message]:
|
| 25 |
try:
|
| 26 |
+
return await handle_flood_wait(m_msg.copy, chat_id=Var.BIN_CHANNEL)
|
| 27 |
+
except Exception as e:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
if "MEDIA_CAPTION_TOO_LONG" in str(e):
|
| 29 |
logger.debug(f"MEDIA_CAPTION_TOO_LONG error, retrying without caption: {e}")
|
| 30 |
+
return await handle_flood_wait(m_msg.copy, chat_id=Var.BIN_CHANNEL, caption=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
logger.error(f"Error fwd_media copy: {e}", exc_info=True)
|
| 32 |
return None
|
| 33 |
|
|
|
|
| 223 |
return links
|
| 224 |
except Exception as e:
|
| 225 |
if status_msg:
|
| 226 |
+
await handle_flood_wait(status_msg.edit_text, MSG_ERROR_PROCESSING_MEDIA)
|
| 227 |
if str(e):
|
| 228 |
await notify_own(bot, MSG_CRITICAL_ERROR.format(
|
| 229 |
error=str(e),
|
|
|
|
| 250 |
except MessageNotModified:
|
| 251 |
pass
|
| 252 |
try:
|
| 253 |
+
messages = await handle_flood_wait(bot.get_messages, msg.chat.id, batch_ids)
|
| 254 |
+
if messages is None:
|
| 255 |
+
messages = []
|
|
|
|
|
|
|
| 256 |
except Exception as e:
|
| 257 |
logger.error(f"Error getting messages in batch: {e}", exc_info=True)
|
| 258 |
messages = []
|
Thunder/server/stream_routes.py
CHANGED
|
@@ -3,10 +3,12 @@
|
|
| 3 |
import re
|
| 4 |
import secrets
|
| 5 |
import time
|
|
|
|
|
|
|
| 6 |
from aiohttp import web
|
| 7 |
-
|
| 8 |
-
from Thunder import
|
| 9 |
-
from Thunder.bot import
|
| 10 |
from Thunder.server.exceptions import FileNotFound, InvalidHash
|
| 11 |
from Thunder.utils.custom_dl import ByteStreamer
|
| 12 |
from Thunder.utils.logger import logger
|
|
|
|
| 3 |
import re
|
| 4 |
import secrets
|
| 5 |
import time
|
| 6 |
+
from urllib.parse import quote, unquote
|
| 7 |
+
|
| 8 |
from aiohttp import web
|
| 9 |
+
|
| 10 |
+
from Thunder import __version__, StartTime
|
| 11 |
+
from Thunder.bot import StreamBot, multi_clients, work_loads
|
| 12 |
from Thunder.server.exceptions import FileNotFound, InvalidHash
|
| 13 |
from Thunder.utils.custom_dl import ByteStreamer
|
| 14 |
from Thunder.utils.logger import logger
|
Thunder/utils/bot_utils.py
CHANGED
|
@@ -1,43 +1,26 @@
|
|
| 1 |
# Thunder/utils/bot_utils.py
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
-
from typing import
|
| 5 |
from urllib.parse import quote
|
| 6 |
|
| 7 |
from pyrogram import Client
|
| 8 |
-
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, User, LinkPreviewOptions
|
| 9 |
from pyrogram.enums import ChatMemberStatus
|
| 10 |
from pyrogram.errors import FloodWait
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
from Thunder.vars import Var
|
| 13 |
from Thunder.utils.database import db
|
|
|
|
|
|
|
| 14 |
from Thunder.utils.human_readable import humanbytes
|
| 15 |
from Thunder.utils.logger import logger
|
| 16 |
-
from Thunder.utils.messages import (
|
| 17 |
-
|
| 18 |
-
MSG_NEW_USER,
|
| 19 |
-
MSG_DC_UNKNOWN,
|
| 20 |
-
MSG_DC_USER_INFO
|
| 21 |
-
)
|
| 22 |
-
from Thunder.utils.file_properties import get_fname, get_fsize, get_hash
|
| 23 |
from Thunder.utils.shortener import shorten
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
-
async def handle_flood_wait(func: Callable, *args, **kwargs):
|
| 27 |
-
try:
|
| 28 |
-
return await func(*args, **kwargs)
|
| 29 |
-
except FloodWait as e:
|
| 30 |
-
logger.debug(f"FloodWait in {func.__name__}: {e.value}s. Retrying...")
|
| 31 |
-
await asyncio.sleep(e.value)
|
| 32 |
-
try:
|
| 33 |
-
return await func(*args, **kwargs)
|
| 34 |
-
except Exception as retry_error:
|
| 35 |
-
logger.error(f"Retry failed in {func.__name__}: {retry_error}")
|
| 36 |
-
return None
|
| 37 |
-
except Exception as e:
|
| 38 |
-
logger.error(f"Error in {func.__name__}: {e}")
|
| 39 |
-
return None
|
| 40 |
-
|
| 41 |
|
| 42 |
async def notify_ch(cli: Client, txt: str):
|
| 43 |
if not (hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0):
|
|
@@ -122,3 +105,7 @@ async def is_admin(cli: Client, chat_id_val: int) -> bool:
|
|
| 122 |
if member is None:
|
| 123 |
return False
|
| 124 |
return member.status in [ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Thunder/utils/bot_utils.py
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
+
from typing import Any, Callable, Dict, Optional
|
| 5 |
from urllib.parse import quote
|
| 6 |
|
| 7 |
from pyrogram import Client
|
|
|
|
| 8 |
from pyrogram.enums import ChatMemberStatus
|
| 9 |
from pyrogram.errors import FloodWait
|
| 10 |
+
from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
|
| 11 |
+
LinkPreviewOptions, Message, User)
|
| 12 |
|
|
|
|
| 13 |
from Thunder.utils.database import db
|
| 14 |
+
from Thunder.utils.file_properties import get_fname, get_fsize, get_hash
|
| 15 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 16 |
from Thunder.utils.human_readable import humanbytes
|
| 17 |
from Thunder.utils.logger import logger
|
| 18 |
+
from Thunder.utils.messages import (MSG_BUTTON_GET_HELP, MSG_DC_UNKNOWN,
|
| 19 |
+
MSG_DC_USER_INFO, MSG_NEW_USER)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
from Thunder.utils.shortener import shorten
|
| 21 |
+
from Thunder.vars import Var
|
| 22 |
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
async def notify_ch(cli: Client, txt: str):
|
| 26 |
if not (hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0):
|
|
|
|
| 105 |
if member is None:
|
| 106 |
return False
|
| 107 |
return member.status in [ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
async def reply(msg: Message, **kwargs):
|
| 111 |
+
return await handle_flood_wait(msg.reply_text, **kwargs, quote=True, link_preview_options=LinkPreviewOptions(is_disabled=True))
|
Thunder/utils/broadcast.py
CHANGED
|
@@ -1,39 +1,36 @@
|
|
| 1 |
# Thunder/utils/broadcast.py
|
| 2 |
|
|
|
|
| 3 |
import os
|
| 4 |
import time
|
| 5 |
-
|
| 6 |
from pyrogram.client import Client
|
| 7 |
from pyrogram.enums import ParseMode
|
| 8 |
-
from pyrogram.
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
from pyrogram.errors import (
|
| 14 |
-
FloodWait,
|
| 15 |
-
UserDeactivated,
|
| 16 |
-
UserIsBlocked,
|
| 17 |
-
PeerIdInvalid,
|
| 18 |
-
ChatWriteForbidden
|
| 19 |
-
)
|
| 20 |
from Thunder.utils.database import db
|
| 21 |
-
from Thunder.utils.
|
| 22 |
from Thunder.utils.logger import logger
|
| 23 |
from Thunder.utils.messages import *
|
|
|
|
|
|
|
| 24 |
|
| 25 |
broadcast_ids = {}
|
| 26 |
|
| 27 |
async def broadcast_message(client: Client, message: Message):
|
| 28 |
if not message.reply_to_message:
|
| 29 |
-
await message.reply_text
|
| 30 |
return
|
| 31 |
|
| 32 |
broadcast_id = os.urandom(3).hex()
|
| 33 |
stats = {"total": 0, "success": 0, "failed": 0, "deleted": 0, "cancelled": False}
|
| 34 |
broadcast_ids[broadcast_id] = stats
|
| 35 |
|
| 36 |
-
status_msg = await
|
|
|
|
| 37 |
MSG_BROADCAST_START,
|
| 38 |
reply_markup=InlineKeyboardMarkup([[
|
| 39 |
InlineKeyboardButton(MSG_BUTTON_CANCEL_BROADCAST, callback_data=f"cancel_{broadcast_id}")
|
|
@@ -48,15 +45,9 @@ async def broadcast_message(client: Client, message: Message):
|
|
| 48 |
if stats["cancelled"]:
|
| 49 |
break
|
| 50 |
try:
|
| 51 |
-
await message.reply_to_message.copy
|
| 52 |
-
stats["success"] += 1
|
| 53 |
-
except FloodWait as e:
|
| 54 |
-
await asyncio.sleep(e.value)
|
| 55 |
-
try:
|
| 56 |
-
await message.reply_to_message.copy(user['id'])
|
| 57 |
stats["success"] += 1
|
| 58 |
-
|
| 59 |
-
logger.error(f"Error copying message to user {user['id']} after FloodWait: {e}", exc_info=True)
|
| 60 |
stats["failed"] += 1
|
| 61 |
except (UserDeactivated, UserIsBlocked, PeerIdInvalid, ChatWriteForbidden) as e:
|
| 62 |
logger.warning(f"User {user['id']} removed due to: {type(e).__name__}", exc_info=True)
|
|
@@ -66,8 +57,9 @@ async def broadcast_message(client: Client, message: Message):
|
|
| 66 |
logger.error(f"Error copying message to user {user['id']}: {e}", exc_info=True)
|
| 67 |
stats["failed"] += 1
|
| 68 |
|
| 69 |
-
await status_msg.delete
|
| 70 |
-
await
|
|
|
|
| 71 |
MSG_BROADCAST_COMPLETE.format(
|
| 72 |
elapsed_time=get_readable_time(int(time.time() - start_time)),
|
| 73 |
total_users=stats["total"],
|
|
|
|
| 1 |
# Thunder/utils/broadcast.py
|
| 2 |
|
| 3 |
+
import asyncio
|
| 4 |
import os
|
| 5 |
import time
|
| 6 |
+
|
| 7 |
from pyrogram.client import Client
|
| 8 |
from pyrogram.enums import ParseMode
|
| 9 |
+
from pyrogram.errors import (ChatWriteForbidden, PeerIdInvalid, UserDeactivated,
|
| 10 |
+
UserIsBlocked)
|
| 11 |
+
from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
|
| 12 |
+
Message)
|
| 13 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
from Thunder.utils.database import db
|
| 15 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 16 |
from Thunder.utils.logger import logger
|
| 17 |
from Thunder.utils.messages import *
|
| 18 |
+
from Thunder.utils.time_format import get_readable_time
|
| 19 |
+
|
| 20 |
|
| 21 |
broadcast_ids = {}
|
| 22 |
|
| 23 |
async def broadcast_message(client: Client, message: Message):
|
| 24 |
if not message.reply_to_message:
|
| 25 |
+
await handle_flood_wait(message.reply_text, MSG_INVALID_BROADCAST_CMD)
|
| 26 |
return
|
| 27 |
|
| 28 |
broadcast_id = os.urandom(3).hex()
|
| 29 |
stats = {"total": 0, "success": 0, "failed": 0, "deleted": 0, "cancelled": False}
|
| 30 |
broadcast_ids[broadcast_id] = stats
|
| 31 |
|
| 32 |
+
status_msg = await handle_flood_wait(
|
| 33 |
+
message.reply_text,
|
| 34 |
MSG_BROADCAST_START,
|
| 35 |
reply_markup=InlineKeyboardMarkup([[
|
| 36 |
InlineKeyboardButton(MSG_BUTTON_CANCEL_BROADCAST, callback_data=f"cancel_{broadcast_id}")
|
|
|
|
| 45 |
if stats["cancelled"]:
|
| 46 |
break
|
| 47 |
try:
|
| 48 |
+
if await handle_flood_wait(message.reply_to_message.copy, user['id']):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
stats["success"] += 1
|
| 50 |
+
else:
|
|
|
|
| 51 |
stats["failed"] += 1
|
| 52 |
except (UserDeactivated, UserIsBlocked, PeerIdInvalid, ChatWriteForbidden) as e:
|
| 53 |
logger.warning(f"User {user['id']} removed due to: {type(e).__name__}", exc_info=True)
|
|
|
|
| 57 |
logger.error(f"Error copying message to user {user['id']}: {e}", exc_info=True)
|
| 58 |
stats["failed"] += 1
|
| 59 |
|
| 60 |
+
await handle_flood_wait(status_msg.delete)
|
| 61 |
+
await handle_flood_wait(
|
| 62 |
+
message.reply_text,
|
| 63 |
MSG_BROADCAST_COMPLETE.format(
|
| 64 |
elapsed_time=get_readable_time(int(time.time() - start_time)),
|
| 65 |
total_users=stats["total"],
|
Thunder/utils/commands.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) Restart the bot",
|
| 22 |
+
"shell": "(Admin) Execute a shell command",
|
| 23 |
+
"users": "(Admin) Show the total number of users",
|
| 24 |
+
"authorize": "(Admin) Grant permanent access to a user",
|
| 25 |
+
"deauthorize": "(Admin) Remove permanent access from a user",
|
| 26 |
+
"listauth": "(Admin) List all authorized users"
|
| 27 |
+
}
|
| 28 |
+
return [BotCommand(name, desc) for name, desc in command_descriptions.items()]
|
| 29 |
+
|
| 30 |
+
async def set_commands():
|
| 31 |
+
if Var.SET_COMMANDS:
|
| 32 |
+
try:
|
| 33 |
+
commands = get_commands()
|
| 34 |
+
if commands:
|
| 35 |
+
await StreamBot.set_bot_commands(commands)
|
| 36 |
+
except Exception as e:
|
| 37 |
+
logger.error(f"Failed to set bot commands: {e}", exc_info=True)
|
Thunder/utils/custom_dl.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
# Thunder/utils/custom_dl.py
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
-
|
| 5 |
-
from typing import Dict, Any, AsyncGenerator
|
| 6 |
|
| 7 |
from pyrogram import Client
|
| 8 |
from pyrogram.errors import FloodWait
|
|
@@ -14,21 +13,22 @@ from Thunder.vars import Var
|
|
| 14 |
|
| 15 |
class ByteStreamer:
|
| 16 |
__slots__ = ('client', 'chat_id')
|
| 17 |
-
|
| 18 |
-
def __init__(self, client: Client):
|
| 19 |
self.client = client
|
| 20 |
self.chat_id = int(Var.BIN_CHANNEL)
|
| 21 |
|
| 22 |
async def get_message(self, message_id: int) -> Message:
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
if not message or not message.media:
|
| 34 |
raise FileNotFound(f"Message {message_id} not found")
|
|
@@ -40,30 +40,28 @@ class ByteStreamer:
|
|
| 40 |
if limit > 0:
|
| 41 |
chunk_offset = offset // (1024 * 1024)
|
| 42 |
chunk_limit = (limit + 1024 * 1024 - 1) // (1024 * 1024)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
yield chunk
|
| 52 |
else:
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
|
| 62 |
def get_file_info_sync(self, message: Message) -> Dict[str, Any]:
|
| 63 |
media = message.document or message.video or message.audio or message.photo
|
| 64 |
if not media:
|
| 65 |
return {"message_id": message.id, "error": "No media"}
|
| 66 |
-
|
| 67 |
return {
|
| 68 |
"message_id": message.id,
|
| 69 |
"file_size": getattr(media, 'file_size', 0) or 0,
|
|
|
|
| 1 |
# Thunder/utils/custom_dl.py
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
+
from typing import Any, AsyncGenerator, Dict
|
|
|
|
| 5 |
|
| 6 |
from pyrogram import Client
|
| 7 |
from pyrogram.errors import FloodWait
|
|
|
|
| 13 |
|
| 14 |
class ByteStreamer:
|
| 15 |
__slots__ = ('client', 'chat_id')
|
| 16 |
+
|
| 17 |
+
def __init__(self, client: Client) -> None:
|
| 18 |
self.client = client
|
| 19 |
self.chat_id = int(Var.BIN_CHANNEL)
|
| 20 |
|
| 21 |
async def get_message(self, message_id: int) -> Message:
|
| 22 |
+
while True:
|
| 23 |
+
try:
|
| 24 |
+
message = await self.client.get_messages(self.chat_id, message_id)
|
| 25 |
+
break
|
| 26 |
+
except FloodWait as e:
|
| 27 |
+
logger.debug(f"FloodWait: get_message, sleep {e.value}s")
|
| 28 |
+
await asyncio.sleep(e.value)
|
| 29 |
+
except Exception as e:
|
| 30 |
+
logger.debug(f"Error fetching message {message_id}: {e}", exc_info=True)
|
| 31 |
+
raise FileNotFound(f"Message {message_id} not found") from e
|
| 32 |
|
| 33 |
if not message or not message.media:
|
| 34 |
raise FileNotFound(f"Message {message_id} not found")
|
|
|
|
| 40 |
if limit > 0:
|
| 41 |
chunk_offset = offset // (1024 * 1024)
|
| 42 |
chunk_limit = (limit + 1024 * 1024 - 1) // (1024 * 1024)
|
| 43 |
+
while True:
|
| 44 |
+
try:
|
| 45 |
+
async for chunk in self.client.stream_media(message, offset=chunk_offset, limit=chunk_limit):
|
| 46 |
+
yield chunk
|
| 47 |
+
break
|
| 48 |
+
except FloodWait as e:
|
| 49 |
+
logger.debug(f"FloodWait: stream_file, sleep {e.value}s")
|
| 50 |
+
await asyncio.sleep(e.value)
|
|
|
|
| 51 |
else:
|
| 52 |
+
while True:
|
| 53 |
+
try:
|
| 54 |
+
async for chunk in self.client.stream_media(message):
|
| 55 |
+
yield chunk
|
| 56 |
+
break
|
| 57 |
+
except FloodWait as e:
|
| 58 |
+
logger.debug(f"FloodWait: stream_file, sleep {e.value}s")
|
| 59 |
+
await asyncio.sleep(e.value)
|
| 60 |
|
| 61 |
def get_file_info_sync(self, message: Message) -> Dict[str, Any]:
|
| 62 |
media = message.document or message.video or message.audio or message.photo
|
| 63 |
if not media:
|
| 64 |
return {"message_id": message.id, "error": "No media"}
|
|
|
|
| 65 |
return {
|
| 66 |
"message_id": message.id,
|
| 67 |
"file_size": getattr(media, 'file_size', 0) or 0,
|
Thunder/utils/decorators.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
| 1 |
# Thunder/utils/decorators.py
|
| 2 |
|
| 3 |
-
from pyrogram.types import
|
| 4 |
-
|
| 5 |
-
from Thunder.utils.logger import logger
|
| 6 |
-
from Thunder.utils.messages import (
|
| 7 |
-
MSG_DECORATOR_BANNED,
|
| 8 |
-
MSG_TOKEN_INVALID,
|
| 9 |
-
MSG_ERROR_UNAUTHORIZED
|
| 10 |
-
)
|
| 11 |
from Thunder.utils.database import db
|
| 12 |
-
from Thunder.utils.
|
|
|
|
|
|
|
|
|
|
| 13 |
from Thunder.utils.shortener import shorten
|
|
|
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
async def check_banned(client, message: Message):
|
|
@@ -19,7 +18,7 @@ async def check_banned(client, message: Message):
|
|
| 19 |
return True
|
| 20 |
user_id = message.from_user.id
|
| 21 |
if user_id == Var.OWNER_ID:
|
| 22 |
-
|
| 23 |
|
| 24 |
ban_details = await db.is_user_banned(user_id)
|
| 25 |
if ban_details:
|
|
@@ -29,7 +28,8 @@ async def check_banned(client, message: Message):
|
|
| 29 |
if banned_at and hasattr(banned_at, 'strftime')
|
| 30 |
else str(banned_at) if banned_at else 'N/A'
|
| 31 |
)
|
| 32 |
-
await
|
|
|
|
| 33 |
MSG_DECORATOR_BANNED.format(
|
| 34 |
reason=ban_details.get('reason', 'Not specified'),
|
| 35 |
ban_time=ban_time
|
|
@@ -60,15 +60,19 @@ async def require_token(client, message: Message):
|
|
| 60 |
temp_token_string = await generate(user_id)
|
| 61 |
except Exception as e:
|
| 62 |
logger.error(f"Failed to generate temporary token for user {user_id} in require_token: {e}", exc_info=True)
|
| 63 |
-
await message.reply_text
|
| 64 |
return False
|
| 65 |
|
| 66 |
if not temp_token_string:
|
| 67 |
logger.error(f"Temporary token generation returned empty for user {user_id} in require_token.", exc_info=True)
|
| 68 |
-
await message.reply_text
|
| 69 |
return False
|
| 70 |
|
| 71 |
-
me = await client.get_me
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
deep_link = f"https://t.me/{me.username}?start={temp_token_string}"
|
| 73 |
short_url = deep_link
|
| 74 |
|
|
@@ -79,7 +83,8 @@ async def require_token(client, message: Message):
|
|
| 79 |
except Exception as e:
|
| 80 |
logger.warning(f"Failed to shorten token link for user {user_id}: {e}. Using full link.", exc_info=True)
|
| 81 |
|
| 82 |
-
await
|
|
|
|
| 83 |
MSG_TOKEN_INVALID,
|
| 84 |
reply_markup=InlineKeyboardMarkup([
|
| 85 |
[InlineKeyboardButton("Activate Access", url=short_url)]
|
|
@@ -91,7 +96,7 @@ async def require_token(client, message: Message):
|
|
| 91 |
except Exception as e:
|
| 92 |
logger.error(f"Error in require_token: {e}", exc_info=True)
|
| 93 |
try:
|
| 94 |
-
await message.reply_text
|
| 95 |
except Exception as inner_e:
|
| 96 |
logger.error(f"Failed to send error message to user in require_token: {inner_e}", exc_info=True)
|
| 97 |
return False
|
|
|
|
| 1 |
# Thunder/utils/decorators.py
|
| 2 |
|
| 3 |
+
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
|
| 4 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from Thunder.utils.database import db
|
| 6 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 7 |
+
from Thunder.utils.logger import logger
|
| 8 |
+
from Thunder.utils.messages import (MSG_DECORATOR_BANNED,
|
| 9 |
+
MSG_ERROR_UNAUTHORIZED, MSG_TOKEN_INVALID)
|
| 10 |
from Thunder.utils.shortener import shorten
|
| 11 |
+
from Thunder.utils.tokens import allowed, check, generate
|
| 12 |
+
from Thunder.vars import Var
|
| 13 |
|
| 14 |
|
| 15 |
async def check_banned(client, message: Message):
|
|
|
|
| 18 |
return True
|
| 19 |
user_id = message.from_user.id
|
| 20 |
if user_id == Var.OWNER_ID:
|
| 21 |
+
return True
|
| 22 |
|
| 23 |
ban_details = await db.is_user_banned(user_id)
|
| 24 |
if ban_details:
|
|
|
|
| 28 |
if banned_at and hasattr(banned_at, 'strftime')
|
| 29 |
else str(banned_at) if banned_at else 'N/A'
|
| 30 |
)
|
| 31 |
+
await handle_flood_wait(
|
| 32 |
+
message.reply_text,
|
| 33 |
MSG_DECORATOR_BANNED.format(
|
| 34 |
reason=ban_details.get('reason', 'Not specified'),
|
| 35 |
ban_time=ban_time
|
|
|
|
| 60 |
temp_token_string = await generate(user_id)
|
| 61 |
except Exception as e:
|
| 62 |
logger.error(f"Failed to generate temporary token for user {user_id} in require_token: {e}", exc_info=True)
|
| 63 |
+
await handle_flood_wait(message.reply_text, "Sorry, could not generate an access token link. Please try again later.", quote=True)
|
| 64 |
return False
|
| 65 |
|
| 66 |
if not temp_token_string:
|
| 67 |
logger.error(f"Temporary token generation returned empty for user {user_id} in require_token.", exc_info=True)
|
| 68 |
+
await handle_flood_wait(message.reply_text, "Sorry, could not generate an access token link. Please try again later.", quote=True)
|
| 69 |
return False
|
| 70 |
|
| 71 |
+
me = await handle_flood_wait(client.get_me)
|
| 72 |
+
if not me:
|
| 73 |
+
logger.error(f"Failed to get bot info for user {user_id} in require_token.", exc_info=True)
|
| 74 |
+
await handle_flood_wait(message.reply_text, "Sorry, an unexpected error occurred. Please try again later.", quote=True)
|
| 75 |
+
return False
|
| 76 |
deep_link = f"https://t.me/{me.username}?start={temp_token_string}"
|
| 77 |
short_url = deep_link
|
| 78 |
|
|
|
|
| 83 |
except Exception as e:
|
| 84 |
logger.warning(f"Failed to shorten token link for user {user_id}: {e}. Using full link.", exc_info=True)
|
| 85 |
|
| 86 |
+
await handle_flood_wait(
|
| 87 |
+
message.reply_text,
|
| 88 |
MSG_TOKEN_INVALID,
|
| 89 |
reply_markup=InlineKeyboardMarkup([
|
| 90 |
[InlineKeyboardButton("Activate Access", url=short_url)]
|
|
|
|
| 96 |
except Exception as e:
|
| 97 |
logger.error(f"Error in require_token: {e}", exc_info=True)
|
| 98 |
try:
|
| 99 |
+
await handle_flood_wait(message.reply_text, "An error occurred while checking your authorization. Please try again.", quote=True)
|
| 100 |
except Exception as inner_e:
|
| 101 |
logger.error(f"Failed to send error message to user in require_token: {inner_e}", exc_info=True)
|
| 102 |
return False
|
Thunder/utils/file_properties.py
CHANGED
|
@@ -1,18 +1,17 @@
|
|
| 1 |
# Thunder/utils/file_properties.py
|
| 2 |
|
| 3 |
-
import asyncio
|
| 4 |
-
|
| 5 |
from datetime import datetime as dt
|
| 6 |
from typing import Any, Optional
|
| 7 |
|
| 8 |
from pyrogram.client import Client
|
| 9 |
-
from pyrogram.errors import FloodWait
|
| 10 |
-
from pyrogram.types import Message
|
| 11 |
from pyrogram.file_id import FileId
|
|
|
|
| 12 |
|
| 13 |
from Thunder.server.exceptions import FileNotFound
|
|
|
|
| 14 |
from Thunder.utils.logger import logger
|
| 15 |
|
|
|
|
| 16 |
def get_media(message: Message) -> Optional[Any]:
|
| 17 |
for attr in ("audio", "document", "photo", "sticker", "animation", "video", "voice", "video_note"):
|
| 18 |
media = getattr(message, attr, None)
|
|
@@ -22,18 +21,22 @@ def get_media(message: Message) -> Optional[Any]:
|
|
| 22 |
return media
|
| 23 |
return None
|
| 24 |
|
|
|
|
| 25 |
def get_uniqid(message: Message) -> Optional[str]:
|
| 26 |
media = get_media(message)
|
| 27 |
return getattr(media, 'file_unique_id', None)
|
| 28 |
|
|
|
|
| 29 |
def get_hash(media_msg: Message) -> str:
|
| 30 |
uniq_id = get_uniqid(media_msg)
|
| 31 |
return uniq_id[:6] if uniq_id else ''
|
| 32 |
|
|
|
|
| 33 |
def get_fsize(message: Message) -> int:
|
| 34 |
media = get_media(message)
|
| 35 |
return getattr(media, 'file_size', 0) if media else 0
|
| 36 |
|
|
|
|
| 37 |
def parse_fid(message: Message) -> Optional[FileId]:
|
| 38 |
media = get_media(message)
|
| 39 |
if media and hasattr(media, 'file_id'):
|
|
@@ -43,18 +46,22 @@ def parse_fid(message: Message) -> Optional[FileId]:
|
|
| 43 |
return None
|
| 44 |
return None
|
| 45 |
|
|
|
|
| 46 |
def get_fname(msg: Message) -> str:
|
| 47 |
media = get_media(msg)
|
| 48 |
fname = None
|
|
|
|
| 49 |
if media:
|
| 50 |
fname = getattr(media, 'file_name', None)
|
|
|
|
| 51 |
if not fname:
|
| 52 |
media_type_str = "unknown_media"
|
| 53 |
if msg.media:
|
| 54 |
media_type_value = msg.media.value
|
| 55 |
if media_type_value:
|
| 56 |
media_type_str = str(media_type_value)
|
| 57 |
-
|
|
|
|
| 58 |
if media and hasattr(media, '_file_type'):
|
| 59 |
file_type = media._file_type
|
| 60 |
if file_type == "photo":
|
|
@@ -71,24 +78,25 @@ def get_fname(msg: Message) -> str:
|
|
| 71 |
ext = "bin"
|
| 72 |
timestamp = dt.now().strftime("%Y%m%d%H%M%S")
|
| 73 |
fname = f"Thunder File To Link_{timestamp}.{ext}"
|
|
|
|
| 74 |
return fname
|
| 75 |
|
|
|
|
| 76 |
async def get_fids(client: Client, chat_id: int, message_id: int) -> FileId:
|
| 77 |
try:
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
except FloodWait as e:
|
| 81 |
-
logger.debug(f"FloodWait: get_fids, sleep {e.value}s")
|
| 82 |
-
await asyncio.sleep(e.value)
|
| 83 |
-
msg = await client.get_messages(chat_id, message_id)
|
| 84 |
if not msg or getattr(msg, 'empty', False):
|
| 85 |
raise FileNotFound("Message not found")
|
|
|
|
| 86 |
media = get_media(msg)
|
| 87 |
if media:
|
| 88 |
if not hasattr(media, 'file_id') or not hasattr(media, 'file_unique_id'):
|
| 89 |
raise FileNotFound("Media metadata incomplete")
|
| 90 |
return FileId.decode(media.file_id)
|
|
|
|
| 91 |
raise FileNotFound("No media in message")
|
|
|
|
| 92 |
except Exception as e:
|
| 93 |
logger.error(f"Error in get_fids: {e}", exc_info=True)
|
| 94 |
raise FileNotFound(str(e))
|
|
|
|
| 1 |
# Thunder/utils/file_properties.py
|
| 2 |
|
|
|
|
|
|
|
| 3 |
from datetime import datetime as dt
|
| 4 |
from typing import Any, Optional
|
| 5 |
|
| 6 |
from pyrogram.client import Client
|
|
|
|
|
|
|
| 7 |
from pyrogram.file_id import FileId
|
| 8 |
+
from pyrogram.types import Message
|
| 9 |
|
| 10 |
from Thunder.server.exceptions import FileNotFound
|
| 11 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 12 |
from Thunder.utils.logger import logger
|
| 13 |
|
| 14 |
+
|
| 15 |
def get_media(message: Message) -> Optional[Any]:
|
| 16 |
for attr in ("audio", "document", "photo", "sticker", "animation", "video", "voice", "video_note"):
|
| 17 |
media = getattr(message, attr, None)
|
|
|
|
| 21 |
return media
|
| 22 |
return None
|
| 23 |
|
| 24 |
+
|
| 25 |
def get_uniqid(message: Message) -> Optional[str]:
|
| 26 |
media = get_media(message)
|
| 27 |
return getattr(media, 'file_unique_id', None)
|
| 28 |
|
| 29 |
+
|
| 30 |
def get_hash(media_msg: Message) -> str:
|
| 31 |
uniq_id = get_uniqid(media_msg)
|
| 32 |
return uniq_id[:6] if uniq_id else ''
|
| 33 |
|
| 34 |
+
|
| 35 |
def get_fsize(message: Message) -> int:
|
| 36 |
media = get_media(message)
|
| 37 |
return getattr(media, 'file_size', 0) if media else 0
|
| 38 |
|
| 39 |
+
|
| 40 |
def parse_fid(message: Message) -> Optional[FileId]:
|
| 41 |
media = get_media(message)
|
| 42 |
if media and hasattr(media, 'file_id'):
|
|
|
|
| 46 |
return None
|
| 47 |
return None
|
| 48 |
|
| 49 |
+
|
| 50 |
def get_fname(msg: Message) -> str:
|
| 51 |
media = get_media(msg)
|
| 52 |
fname = None
|
| 53 |
+
|
| 54 |
if media:
|
| 55 |
fname = getattr(media, 'file_name', None)
|
| 56 |
+
|
| 57 |
if not fname:
|
| 58 |
media_type_str = "unknown_media"
|
| 59 |
if msg.media:
|
| 60 |
media_type_value = msg.media.value
|
| 61 |
if media_type_value:
|
| 62 |
media_type_str = str(media_type_value)
|
| 63 |
+
|
| 64 |
+
ext = "bin"
|
| 65 |
if media and hasattr(media, '_file_type'):
|
| 66 |
file_type = media._file_type
|
| 67 |
if file_type == "photo":
|
|
|
|
| 78 |
ext = "bin"
|
| 79 |
timestamp = dt.now().strftime("%Y%m%d%H%M%S")
|
| 80 |
fname = f"Thunder File To Link_{timestamp}.{ext}"
|
| 81 |
+
|
| 82 |
return fname
|
| 83 |
|
| 84 |
+
|
| 85 |
async def get_fids(client: Client, chat_id: int, message_id: int) -> FileId:
|
| 86 |
try:
|
| 87 |
+
msg = await handle_flood_wait(client.get_messages, chat_id, message_id)
|
| 88 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if not msg or getattr(msg, 'empty', False):
|
| 90 |
raise FileNotFound("Message not found")
|
| 91 |
+
|
| 92 |
media = get_media(msg)
|
| 93 |
if media:
|
| 94 |
if not hasattr(media, 'file_id') or not hasattr(media, 'file_unique_id'):
|
| 95 |
raise FileNotFound("Media metadata incomplete")
|
| 96 |
return FileId.decode(media.file_id)
|
| 97 |
+
|
| 98 |
raise FileNotFound("No media in message")
|
| 99 |
+
|
| 100 |
except Exception as e:
|
| 101 |
logger.error(f"Error in get_fids: {e}", exc_info=True)
|
| 102 |
raise FileNotFound(str(e))
|
Thunder/utils/force_channel.py
CHANGED
|
@@ -3,12 +3,13 @@
|
|
| 3 |
import asyncio
|
| 4 |
|
| 5 |
from pyrogram import Client
|
| 6 |
-
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
|
| 7 |
from pyrogram.errors import FloodWait, UserNotParticipant
|
|
|
|
| 8 |
|
| 9 |
-
from Thunder.
|
| 10 |
from Thunder.utils.logger import logger
|
| 11 |
from Thunder.utils.messages import MSG_COMMUNITY_CHANNEL
|
|
|
|
| 12 |
|
| 13 |
_force_link = None
|
| 14 |
_force_title = None
|
|
@@ -23,9 +24,10 @@ async def get_force_info(bot: Client):
|
|
| 23 |
return _force_link, _force_title
|
| 24 |
|
| 25 |
try:
|
| 26 |
-
chat = await bot.get_chat
|
| 27 |
-
|
| 28 |
-
|
|
|
|
| 29 |
return _force_link, _force_title
|
| 30 |
except Exception as e:
|
| 31 |
logger.error(f"Force channel error: {e}", exc_info=True)
|
|
@@ -35,24 +37,30 @@ async def force_channel_check(client: Client, message: Message):
|
|
| 35 |
if not Var.FORCE_CHANNEL_ID:
|
| 36 |
return True
|
| 37 |
try:
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
except UserNotParticipant:
|
| 44 |
link, title = await get_force_info(client)
|
| 45 |
if link and title:
|
| 46 |
-
await
|
|
|
|
| 47 |
MSG_COMMUNITY_CHANNEL.format(channel_title=title),
|
| 48 |
reply_markup=InlineKeyboardMarkup([[
|
| 49 |
InlineKeyboardButton("Join", url=link)
|
| 50 |
]])
|
| 51 |
)
|
| 52 |
else:
|
| 53 |
-
await message.reply_text
|
| 54 |
return False
|
| 55 |
except Exception as e:
|
| 56 |
logger.error(f"Error checking force channel: {e}", exc_info=True)
|
| 57 |
-
await message.reply_text
|
| 58 |
return False
|
|
|
|
| 3 |
import asyncio
|
| 4 |
|
| 5 |
from pyrogram import Client
|
|
|
|
| 6 |
from pyrogram.errors import FloodWait, UserNotParticipant
|
| 7 |
+
from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
|
| 8 |
|
| 9 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 10 |
from Thunder.utils.logger import logger
|
| 11 |
from Thunder.utils.messages import MSG_COMMUNITY_CHANNEL
|
| 12 |
+
from Thunder.vars import Var
|
| 13 |
|
| 14 |
_force_link = None
|
| 15 |
_force_title = None
|
|
|
|
| 24 |
return _force_link, _force_title
|
| 25 |
|
| 26 |
try:
|
| 27 |
+
chat = await handle_flood_wait(bot.get_chat, Var.FORCE_CHANNEL_ID)
|
| 28 |
+
if chat:
|
| 29 |
+
_force_link = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
|
| 30 |
+
_force_title = chat.title or "Channel"
|
| 31 |
return _force_link, _force_title
|
| 32 |
except Exception as e:
|
| 33 |
logger.error(f"Force channel error: {e}", exc_info=True)
|
|
|
|
| 37 |
if not Var.FORCE_CHANNEL_ID:
|
| 38 |
return True
|
| 39 |
try:
|
| 40 |
+
while True:
|
| 41 |
+
try:
|
| 42 |
+
member = await client.get_chat_member(Var.FORCE_CHANNEL_ID, message.from_user.id)
|
| 43 |
+
if member is None:
|
| 44 |
+
logger.error(f"Failed to get chat member for {message.from_user.id} in force channel {Var.FORCE_CHANNEL_ID} after retries.")
|
| 45 |
+
return False
|
| 46 |
+
return True
|
| 47 |
+
except FloodWait as e:
|
| 48 |
+
logger.debug(f"FloodWait in force_channel_check, sleeping for {e.value}s")
|
| 49 |
+
await asyncio.sleep(e.value)
|
| 50 |
except UserNotParticipant:
|
| 51 |
link, title = await get_force_info(client)
|
| 52 |
if link and title:
|
| 53 |
+
await handle_flood_wait(
|
| 54 |
+
message.reply_text,
|
| 55 |
MSG_COMMUNITY_CHANNEL.format(channel_title=title),
|
| 56 |
reply_markup=InlineKeyboardMarkup([[
|
| 57 |
InlineKeyboardButton("Join", url=link)
|
| 58 |
]])
|
| 59 |
)
|
| 60 |
else:
|
| 61 |
+
await handle_flood_wait(message.reply_text, "You must join the channel to use this bot.")
|
| 62 |
return False
|
| 63 |
except Exception as e:
|
| 64 |
logger.error(f"Error checking force channel: {e}", exc_info=True)
|
| 65 |
+
await handle_flood_wait(message.reply_text, "An unexpected error occurred while checking channel membership. Please try again.")
|
| 66 |
return False
|
Thunder/utils/handler.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Thunder/utils/handler.py
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
from typing import Callable
|
| 5 |
+
|
| 6 |
+
from pyrogram.errors import FloodWait
|
| 7 |
+
|
| 8 |
+
from Thunder.utils.logger import logger
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
async def handle_flood_wait(func: Callable, *args, **kwargs):
|
| 12 |
+
retries = kwargs.pop('retries', 5)
|
| 13 |
+
delay = kwargs.pop('delay', 5)
|
| 14 |
+
|
| 15 |
+
for i in range(retries):
|
| 16 |
+
try:
|
| 17 |
+
return await func(*args, **kwargs)
|
| 18 |
+
except FloodWait as e:
|
| 19 |
+
wait_time = e.value
|
| 20 |
+
logger.debug(f"FloodWait encountered in '{func.__name__}'. Waiting for {wait_time}s. Retry {i + 1}/{retries}.")
|
| 21 |
+
await asyncio.sleep(wait_time)
|
| 22 |
+
except Exception:
|
| 23 |
+
logger.error(f"An exception occurred in '{func.__name__}' on retry {i + 1}/{retries}", exc_info=True)
|
| 24 |
+
if i < retries - 1:
|
| 25 |
+
await asyncio.sleep(delay)
|
| 26 |
+
else:
|
| 27 |
+
raise
|
| 28 |
+
return None
|
Thunder/utils/render_template.py
CHANGED
|
@@ -1,12 +1,14 @@
|
|
| 1 |
# Thunder/utils/render_template.py
|
| 2 |
|
| 3 |
-
import urllib.parse
|
| 4 |
import html as html_module
|
|
|
|
| 5 |
|
| 6 |
from jinja2 import Environment, FileSystemLoader
|
|
|
|
| 7 |
from Thunder.bot import StreamBot
|
| 8 |
from Thunder.server.exceptions import InvalidHash
|
| 9 |
-
from Thunder.utils.file_properties import
|
|
|
|
| 10 |
from Thunder.utils.logger import logger
|
| 11 |
from Thunder.vars import Var
|
| 12 |
|
|
@@ -20,7 +22,7 @@ template_env = Environment(
|
|
| 20 |
|
| 21 |
async def render_page(id: int, secure_hash: str, requested_action: str | None = None) -> str:
|
| 22 |
try:
|
| 23 |
-
message = await StreamBot.get_messages
|
| 24 |
if not message:
|
| 25 |
raise InvalidHash("Message not found")
|
| 26 |
|
|
|
|
| 1 |
# Thunder/utils/render_template.py
|
| 2 |
|
|
|
|
| 3 |
import html as html_module
|
| 4 |
+
import urllib.parse
|
| 5 |
|
| 6 |
from jinja2 import Environment, FileSystemLoader
|
| 7 |
+
|
| 8 |
from Thunder.bot import StreamBot
|
| 9 |
from Thunder.server.exceptions import InvalidHash
|
| 10 |
+
from Thunder.utils.file_properties import get_fname, get_uniqid
|
| 11 |
+
from Thunder.utils.handler import handle_flood_wait
|
| 12 |
from Thunder.utils.logger import logger
|
| 13 |
from Thunder.vars import Var
|
| 14 |
|
|
|
|
| 22 |
|
| 23 |
async def render_page(id: int, secure_hash: str, requested_action: str | None = None) -> str:
|
| 24 |
try:
|
| 25 |
+
message = await handle_flood_wait(StreamBot.get_messages, chat_id=int(Var.BIN_CHANNEL), message_ids=id)
|
| 26 |
if not message:
|
| 27 |
raise InvalidHash("Message not found")
|
| 28 |
|
Thunder/vars.py
CHANGED
|
@@ -27,7 +27,7 @@ class Var:
|
|
| 27 |
raise ValueError("Missing required Telegram API configuration")
|
| 28 |
|
| 29 |
NAME: str = os.getenv("NAME", "ThunderF2L")
|
| 30 |
-
SLEEP_THRESHOLD: int = int(os.getenv("SLEEP_THRESHOLD", "
|
| 31 |
WORKERS: int = int(os.getenv("WORKERS", "8"))
|
| 32 |
TIMEOUT: int = int(os.getenv("TIMEOUT", "90"))
|
| 33 |
|
|
@@ -46,7 +46,7 @@ class Var:
|
|
| 46 |
OWNER_ID: int = int(os.getenv("OWNER_ID", ""))
|
| 47 |
|
| 48 |
if not OWNER_ID:
|
| 49 |
-
logger.warning("WARNING: OWNER_ID is
|
| 50 |
|
| 51 |
OWNER_USERNAME: str = os.getenv("OWNER_USERNAME", "")
|
| 52 |
|
|
@@ -56,6 +56,8 @@ class Var:
|
|
| 56 |
PORT_SEGMENT: str = "" if NO_PORT else f":{PORT}"
|
| 57 |
URL: str = f"{PROTOCOL}://{FQDN}{PORT_SEGMENT}/"
|
| 58 |
|
|
|
|
|
|
|
| 59 |
DATABASE_URL: str = os.getenv("DATABASE_URL", "")
|
| 60 |
|
| 61 |
if not DATABASE_URL:
|
|
|
|
| 27 |
raise ValueError("Missing required Telegram API configuration")
|
| 28 |
|
| 29 |
NAME: str = os.getenv("NAME", "ThunderF2L")
|
| 30 |
+
SLEEP_THRESHOLD: int = int(os.getenv("SLEEP_THRESHOLD", "300"))
|
| 31 |
WORKERS: int = int(os.getenv("WORKERS", "8"))
|
| 32 |
TIMEOUT: int = int(os.getenv("TIMEOUT", "90"))
|
| 33 |
|
|
|
|
| 46 |
OWNER_ID: int = int(os.getenv("OWNER_ID", ""))
|
| 47 |
|
| 48 |
if not OWNER_ID:
|
| 49 |
+
logger.warning("WARNING: OWNER_ID is not set. No user will be granted owner access.")
|
| 50 |
|
| 51 |
OWNER_USERNAME: str = os.getenv("OWNER_USERNAME", "")
|
| 52 |
|
|
|
|
| 56 |
PORT_SEGMENT: str = "" if NO_PORT else f":{PORT}"
|
| 57 |
URL: str = f"{PROTOCOL}://{FQDN}{PORT_SEGMENT}/"
|
| 58 |
|
| 59 |
+
SET_COMMANDS: bool = str_to_bool(os.getenv("SET_COMMANDS", "True"))
|
| 60 |
+
|
| 61 |
DATABASE_URL: str = os.getenv("DATABASE_URL", "")
|
| 62 |
|
| 63 |
if not DATABASE_URL:
|
config_sample.env
CHANGED
|
@@ -35,6 +35,9 @@ NO_PORT="True" # Hide port in URLs ("True" or "False")
|
|
| 35 |
|
| 36 |
MAX_BATCH_FILES=50
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
# Force users to join a specific channel before using the bot
|
| 39 |
FORCE_CHANNEL_ID="" # Example: -1001234567890 (Leave empty if not needed)
|
| 40 |
|
|
@@ -76,7 +79,7 @@ URL_SHORTENER_SITE="" # Example: "example.com"
|
|
| 76 |
NAME="ThunderF2L" # Bot application name
|
| 77 |
|
| 78 |
# Performance settings
|
| 79 |
-
SLEEP_THRESHOLD=
|
| 80 |
WORKERS=8 # Number of worker processes
|
| 81 |
|
| 82 |
# Web server configuration
|
|
|
|
| 35 |
|
| 36 |
MAX_BATCH_FILES=50
|
| 37 |
|
| 38 |
+
# Set bot commands on startup (True/False)
|
| 39 |
+
SET_COMMANDS="True"
|
| 40 |
+
|
| 41 |
# Force users to join a specific channel before using the bot
|
| 42 |
FORCE_CHANNEL_ID="" # Example: -1001234567890 (Leave empty if not needed)
|
| 43 |
|
|
|
|
| 79 |
NAME="ThunderF2L" # Bot application name
|
| 80 |
|
| 81 |
# Performance settings
|
| 82 |
+
SLEEP_THRESHOLD=300 # Sleep time in seconds
|
| 83 |
WORKERS=8 # Number of worker processes
|
| 84 |
|
| 85 |
# Web server configuration
|