Fyaz Mohammed commited on
Commit
9a92ff5
·
unverified ·
1 Parent(s): d3ac0b2

feat: 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 CHANGED
@@ -43,7 +43,8 @@
43
 
44
  **Perfect for:**
45
 
46
- - 📤 Download Telegram media at high speed.
 
47
  - 🎬 Content creators sharing media files.
48
  - 👥 Communities distributing resources.
49
  - 🎓 Educational platforms sharing materials.
@@ -53,7 +54,7 @@
53
 
54
  ## ✨ Features
55
 
56
- ### 🚀 Core Functionality
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 | `120` | `30` |
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
- Paste the following into the BotFather "Edit Commands" section for your bot:
 
 
230
 
231
  ```text
232
  start - Start the bot and get a welcome message
233
- link - Generate a direct link for a file (supports batch in groups)
234
- dc - Get the data center (DC) of a user or file
235
- ping - Check if the bot is online
236
  about - Get information about the bot
237
  help - Show help and usage instructions
238
- status - (Admin) Check bot status, uptime, and resource usage
 
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.1"
 
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
- from aiohttp import web
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 initialize_clients, cleanup_clients
 
 
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
- try:
92
- await StreamBot.start()
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
- try:
105
- await StreamBot.edit_message_text(
106
- chat_id=restart_message_data["chat_id"],
107
- message_id=restart_message_data["message_id"],
108
- text=MSG_ADMIN_RESTART_DONE
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.vars import Var
9
  from Thunder.utils.config_parser import TokenParser
10
- from Thunder.bot import multi_clients, work_loads, StreamBot
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
- sleep_threshold=Var.SLEEP_THRESHOLD,
49
- in_memory=True
 
 
50
  )
51
- try:
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.errors import FloodWait
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.bot.plugins.common import retry, reply
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 retry(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 retry(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,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 retry(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 retry(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,72 +99,69 @@ async def show_stats(client: Client, message: Message):
99
  download=humanbytes(net_io_counters.bytes_recv)
100
  )
101
 
102
- await retry(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 retry(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 retry(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 retry(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 message.reply_document(LOG_FILE, caption=MSG_LOG_FILE_CAPTION)
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 retry(reply, message, text=MSG_ERROR_GENERIC)
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 retry(reply, message, text=MSG_AUTHORIZE_USAGE, parse_mode=ParseMode.MARKDOWN)
135
 
136
  try:
137
  user_id = int(message.command[1])
138
  success = await authorize(user_id, message.from_user.id)
139
- await retry(reply, message,
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 retry(reply, message, text=MSG_INVALID_USER_ID)
143
  except Exception as e:
144
  logger.error(f"Error in authorize_command: {e}", exc_info=True)
145
- await retry(reply, message, text=MSG_ERROR_GENERIC)
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 retry(reply, message, text=MSG_DEAUTHORIZE_USAGE, parse_mode=ParseMode.MARKDOWN)
151
 
152
  try:
153
  user_id = int(message.command[1])
154
  success = await deauthorize(user_id)
155
- await retry(reply, message,
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 retry(reply, message, text=MSG_INVALID_USER_ID)
159
  except Exception as e:
160
  logger.error(f"Error in deauthorize_command: {e}", exc_info=True)
161
- await retry(reply, message, text=MSG_ERROR_GENERIC)
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 retry(reply, message, text=MSG_NO_AUTH_USERS)
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 retry(reply, message,
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 retry(reply, message, text=MSG_BAN_USAGE)
186
 
187
  try:
188
  user_id = int(message.command[1])
189
  if user_id == Var.OWNER_ID:
190
- return await retry(reply, message, text=MSG_CANNOT_BAN_OWNER)
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 retry(reply, message, text=text)
205
 
206
  try:
207
- await client.send_message(user_id, MSG_USER_BANNED_NOTIFICATION)
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 retry(reply, message, text=MSG_INVALID_USER_ID)
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 retry(reply, message, text=MSG_UNBAN_USAGE)
221
 
222
  try:
223
  user_id = int(message.command[1])
224
  if await db.remove_banned_user(user_id=user_id):
225
- await retry(reply, message, text=MSG_ADMIN_USER_UNBANNED.format(user_id=user_id))
226
  try:
227
- await client.send_message(user_id, MSG_USER_UNBANNED_NOTIFICATION)
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 retry(reply, message, text=MSG_USER_NOT_IN_BAN_LIST.format(user_id=user_id))
235
  except ValueError:
236
- await retry(reply, message, text=MSG_INVALID_USER_ID)
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 retry(reply, message, text=MSG_SHELL_USAGE, parse_mode=ParseMode.HTML)
242
 
243
  command = " ".join(message.command[1:])
244
- status_msg = await retry(reply, message,
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
- try:
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
- try:
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
- try:
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(MSG_SHELL_ERROR.format(error=html.escape(str(e))), parse_mode=ParseMode.HTML)
 
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.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, LinkPreviewOptions
6
- from pyrogram.errors import MessageNotModified, FloodWait
 
7
 
8
  from Thunder.bot import StreamBot
9
- from Thunder.vars import Var
 
 
10
  from Thunder.utils.logger import logger
11
  from Thunder.utils.messages import *
12
- from Thunder.utils.decorators import owner_only
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
- max_retries = 2
19
- for attempt in range(max_retries):
20
- try:
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
- break
29
- except FloodWait as e:
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 callback_query.message.edit_text(
 
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("An error occurred. Please try again.", show_alert=True)
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 callback_query.message.edit_text(
 
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("An error occurred. Please try again.", show_alert=True)
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(MSG_ERROR_BROADCAST_RESTART, show_alert=True)
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 callback_query.message.edit_text(
 
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("An error occurred. Please try again.", show_alert=True)
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 callback_query.message.edit_text(
 
121
  MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id)
122
  )
123
  else:
124
- await 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 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 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)
 
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.errors import FloodWait
8
- from pyrogram.types import (
9
- InlineKeyboardButton,
10
- InlineKeyboardMarkup,
11
- LinkPreviewOptions,
12
- Message,
13
- User,
14
- )
15
 
16
  from Thunder.bot import StreamBot
17
- from Thunder.vars import Var
 
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.bot_utils import gen_links, log_newusr, reply_user_err, gen_dc_txt, get_user
 
22
  from Thunder.utils.logger import logger
23
  from Thunder.utils.messages import *
24
- from Thunder.utils.human_readable import humanbytes
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 retry(reply, msg, text=MSG_TOKEN_FAILED.format(
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 retry(reply, msg, text=MSG_TOKEN_FAILED.format(
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 retry(reply, msg, text=MSG_TOKEN_ACTIVATED.format(duration_hours=hrs))
78
  else:
79
- return await retry(reply, msg, text=MSG_TOKEN_INVALID)
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 retry(reply, msg, text=txt, reply_markup=InlineKeyboardMarkup(btns))
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 retry(reply, msg, text=txt, reply_markup=InlineKeyboardMarkup(btns))
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 retry(reply, msg, text=MSG_ABOUT, reply_markup=InlineKeyboardMarkup(btns))
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 retry(reply, msg, text=txt, reply_markup=InlineKeyboardMarkup(btns))
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 retry(reply, msg, text=txt, reply_markup=InlineKeyboardMarkup(btns))
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 retry(reply, msg, text=MSG_PING_START)
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 retry(sent.edit_text,
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 Optional, Dict, Any
5
- from pyrogram import Client, filters, enums
6
- from pyrogram.errors import FloodWait, MessageNotModified, RPCError
7
- from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message, LinkPreviewOptions
 
 
 
8
  from Thunder.bot import StreamBot
 
 
9
  from Thunder.utils.database import db
10
- from Thunder.utils.messages import *
 
 
 
11
  from Thunder.utils.logger import logger
 
12
  from Thunder.vars import Var
13
- from Thunder.utils.decorators import check_banned, require_token, get_shortener_status
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(chat_id=Var.BIN_CHANNEL)
20
- except FloodWait as e:
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
- try:
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(MSG_ERROR_PROCESSING_MEDIA)
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(msg.chat.id, batch_ids)
263
- except FloodWait as e:
264
- logger.warning(f"FloodWait: process_batch get_messages, sleep {e.value}s")
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
- from urllib.parse import unquote, quote
8
- from Thunder import StartTime, __version__
9
- from Thunder.bot import multi_clients, StreamBot, work_loads
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 Optional, Dict, Any, Callable
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
- MSG_BUTTON_GET_HELP,
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
- import asyncio
6
  from pyrogram.client import Client
7
  from pyrogram.enums import ParseMode
8
- from pyrogram.types import (
9
- InlineKeyboardButton,
10
- InlineKeyboardMarkup,
11
- Message
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.time_format import get_readable_time
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(MSG_INVALID_BROADCAST_CMD)
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 message.reply_text(
 
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(user['id'])
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
- except Exception as e:
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 message.reply_text(
 
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
- try:
24
- message = await self.client.get_messages(self.chat_id, message_id)
25
- except FloodWait as e:
26
- logger.debug(f"FloodWait: get_message, sleep {e.value}s")
27
- await asyncio.sleep(e.value)
28
- message = await self.client.get_messages(self.chat_id, message_id)
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,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
- try:
45
- async for chunk in self.client.stream_media(message, offset=chunk_offset, limit=chunk_limit):
46
- yield chunk
47
- except FloodWait as e:
48
- logger.debug(f"FloodWait: stream_file, sleep {e.value}s")
49
- await asyncio.sleep(e.value)
50
- async for chunk in self.client.stream_media(message, offset=chunk_offset, limit=chunk_limit):
51
- yield chunk
52
  else:
53
- try:
54
- async for chunk in self.client.stream_media(message):
55
- yield chunk
56
- except FloodWait as e:
57
- logger.debug(f"FloodWait: stream_file, sleep {e.value}s")
58
- await asyncio.sleep(e.value)
59
- async for chunk in self.client.stream_media(message):
60
- yield chunk
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 Message, InlineKeyboardButton, InlineKeyboardMarkup
4
- from Thunder.vars import Var
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.tokens import check, allowed, generate
 
 
 
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
- return True
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 message.reply_text(
 
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("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 message.reply_text("Sorry, could not generate an access token link. Please try again later.", quote=True)
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 message.reply_text(
 
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("An error occurred while checking your authorization. Please try again.", quote=True)
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
- ext = ""
 
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
- try:
79
- msg = await client.get_messages(chat_id, message_id)
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.vars import Var
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(Var.FORCE_CHANNEL_ID)
27
- _force_link = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
28
- _force_title = chat.title or "Channel"
 
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
- await client.get_chat_member(Var.FORCE_CHANNEL_ID, message.from_user.id)
39
- return True
40
- except FloodWait as e:
41
- await asyncio.sleep(e.value)
42
- return await force_channel_check(client, message)
 
 
 
 
 
43
  except UserNotParticipant:
44
  link, title = await get_force_info(client)
45
  if link and title:
46
- await message.reply_text(
 
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("You must join the channel to use this bot.")
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("An unexpected error occurred while checking channel membership. Please try again.")
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 get_uniqid, get_fname
 
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(chat_id=int(Var.BIN_CHANNEL), message_ids=id)
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", "120"))
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 empty. No user will have admin access.")
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=120 # Sleep time in seconds
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