Fyaz Mohammed commited on
Commit
1fa270a
·
unverified ·
1 Parent(s): 7f1478e

- Updated dependency: migrated from Motor to PyMongo
- Replaced centralized flood-wait handling with per-instance custom logic
- Added public Docker support for easier deployment
• One-click deploy button for Koyeb
• Deployment guides for Render and Railway
- Expanded FAQ section

.github/workflows/dockerize.yml ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker Build & Push
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+
14
+ - name: Login to Docker Hub
15
+ uses: docker/login-action@v3
16
+ with:
17
+ username: fyaz05
18
+ password: ${{ secrets.DOCKER_TOKEN }}
19
+
20
+ - name: Build and Push
21
+ uses: docker/build-push-action@v6
22
+ with:
23
+ context: .
24
+ push: true
25
+ tags: fyaz05/thunder:latest
README.md CHANGED
@@ -21,9 +21,6 @@
21
  - [About The Project](#about-the-project)
22
  - [How It Works](#how-it-works)
23
  - [Features](#features)
24
- - [Getting Started](#getting-started)
25
- - [Prerequisites](#prerequisites)
26
- - [Installation](#installation)
27
  - [Configuration](#configuration)
28
  - [Essential Configuration](#essential-configuration)
29
  - [Optional Configuration](#optional-configuration)
@@ -34,8 +31,14 @@
34
  - [Token System](#token-system)
35
  - [URL Shortening](#url-shortening)
36
  - [Rate Limiting System](#rate-limiting-system)
37
- - [Production Deployment Guide](#production-deployment-guide)
38
- - [Custom Domain & FQDN System](#custom-domain--fqdn-system)
 
 
 
 
 
 
39
  - [Reverse Proxy Setup](#reverse-proxy-setup)
40
  - [Support & Community](#support--community)
41
  - [Troubleshooting & FAQ](#troubleshooting--faq)
@@ -95,65 +98,6 @@ User Uploads File → Telegram Bot → Forwards to Channel → Generates Direct
95
  - 🎨 **Custom Templates** - Personalize messages sent by the bot to match your brand.
96
  - 📈 **Media Info Display** - Shows file size, duration, and format details in the response message.
97
 
98
- ## Getting Started
99
-
100
- This section covers the system requirements and step-by-step installation guide for setting up Thunder.
101
-
102
- ### Prerequisites
103
-
104
- | Requirement | Description | Source |
105
- | :--- | :--- | :--- |
106
- | Python 3.13 | Programming language | [python.org](https://python.org) |
107
- | MongoDB | Database | [mongodb.com](https://mongodb.com) |
108
- | Telegram API | API credentials | [my.telegram.org](https://my.telegram.org/apps) |
109
- | Bot Token | From @BotFather | [@BotFather](https://t.me/BotFather) |
110
- | Public Server | VPS/Dedicated server | Any provider |
111
- | Storage Channel | For file storage | Create in Telegram |
112
-
113
- ### Installation
114
-
115
- #### Docker Installation (Recommended)
116
-
117
- ```bash
118
- # 1. Clone repository
119
- git clone https://github.com/fyaz05/FileToLink.git
120
- cd FileToLink
121
-
122
- # 2. Configure
123
- cp config_sample.env config.env
124
- nano config.env # Edit your settings
125
-
126
- # 3. Build and run
127
- docker build -t thunder .
128
- docker run -d --name thunder -p 8080:8080 thunder
129
- ```
130
-
131
- <details>
132
- <summary>Manual Installation</summary>
133
-
134
- ```bash
135
- # 1. Clone repository
136
- git clone https://github.com/fyaz05/FileToLink.git
137
- cd FileToLink
138
-
139
- # 2. Setup virtual environment
140
- python3 -m venv venv
141
- source venv/bin/activate # Windows: venv\Scripts\activate
142
-
143
- # 3. Install dependencies
144
- pip install -r requirements.txt
145
-
146
- # 4. Configure
147
- cp config_sample.env config.env
148
- nano config.env
149
-
150
- # 5. Run bot
151
- python -m Thunder
152
- ```
153
-
154
- > **Tip:** Start with the essential configuration to get Thunder running, then add optional features as needed.
155
-
156
- </details>
157
 
158
  ## Configuration
159
 
@@ -168,15 +112,16 @@ Copy `config_sample.env` to `config.env` and fill in your values.
168
  | `BOT_TOKEN` | Bot token from @BotFather | `123456:ABCdefGHI` |
169
  | `BIN_CHANNEL` | Storage channel ID | `-1001234567890` |
170
  | `OWNER_ID` | Owner user ID | `12345678` |
171
- | `OWNER_USERNAME` | Owner username | `yourusername` |
172
  | `DATABASE_URL` | MongoDB connection | `mongodb+srv://...` |
173
  | `FQDN` | Domain/IP address | `f2l.thunder.com` |
174
  | `HAS_SSL` | HTTPS enabled | `True` or `False` |
175
  | `PORT` | Server port | `8080` |
176
  | `NO_PORT` | Hide port in URLs | `True` or `False` |
177
 
 
 
178
  <details>
179
- <summary><h3>Optional Configuration</h3></summary>
180
 
181
  | Variable | Description | Default |
182
  | :--- | :--- | :--- |
@@ -324,19 +269,95 @@ Monitor server performance with built-in speed testing:
324
 
325
  Features include download/upload speeds, latency measurements, and shareable result images for performance monitoring.
326
 
327
- ## Production Deployment Guide
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
 
329
- ### Custom Domain & FQDN System
 
 
 
 
 
330
 
331
- Thunder provides enterprise-grade domain configuration through the `FQDN` setting, enabling professional file sharing with custom domains and comprehensive SSL support.
 
 
332
 
333
- - **Custom Domain Support**: Set your primary domain for file sharing.
334
- - **Protocol Selection**: Dynamically serves over HTTP/HTTPS based on your `HAS_SSL` configuration.
335
- - **Cloudflare Compatible**: Full support for Cloudflare Origin Certificates and reverse proxies.
336
- - **Security Headers**: Adds proper security headers for production deployments.
337
 
338
  <details>
339
- <summary><h3>Reverse Proxy Setup</h3></summary>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
  This guide will help you set up a secure reverse proxy using **NGINX** for your file streaming bot with **Cloudflare SSL protection**.
342
 
@@ -433,23 +454,54 @@ Your reverse proxy is now securely streaming files behind Cloudflare!
433
 
434
  ### Troubleshooting & FAQ
435
 
436
- **Bot not responding?**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
- - Check if the `BOT_TOKEN` is correct in your `config.env`.
439
- - Ensure your server's firewall is not blocking the bot's port.
440
- - View bot logs for any error messages.
441
 
442
- **Links not working?**
 
 
 
 
443
 
444
- - Verify your `FQDN` and `PORT` settings are correct.
445
- - If using SSL (`HAS_SSL=True`), ensure your reverse proxy and certificates are set up correctly.
446
- - Check that the bot has admin rights in the `BIN_CHANNEL` and can forward messages there.
447
 
448
- **Q: How do I get my API credentials?**
449
- A: Visit [my.telegram.org/apps](https://my.telegram.org/apps) to get your `API_ID` and `API_HASH`.
 
450
 
451
- **Q: Where can I create a bot token?**
452
- A: Use [@BotFather](https://t.me/BotFather) on Telegram to create and manage your bot.
453
 
454
  ### Contributing
455
 
@@ -469,7 +521,7 @@ Licensed under the [Apache License 2.0](LICENSE). See the `LICENSE` file for det
469
 
470
  - [Pyrofork](https://github.com/Mayuri-Chan/pyrofork) - Telegram MTProto API Framework
471
  - [aiohttp](https://github.com/aio-libs/aiohttp) - Asynchronous HTTP Client/Server
472
- - [Motor](https://github.com/mongodb/motor) - Asynchronous MongoDB Driver
473
  - [TgCrypto](https://github.com/pyrogram/tgcrypto) - High-performance cryptography library
474
 
475
  ## ⚠️ Disclaimer
 
21
  - [About The Project](#about-the-project)
22
  - [How It Works](#how-it-works)
23
  - [Features](#features)
 
 
 
24
  - [Configuration](#configuration)
25
  - [Essential Configuration](#essential-configuration)
26
  - [Optional Configuration](#optional-configuration)
 
31
  - [Token System](#token-system)
32
  - [URL Shortening](#url-shortening)
33
  - [Rate Limiting System](#rate-limiting-system)
34
+ - [Network Speed Testing](#network-speed-testing)
35
+ - [Deployment Guide](#deployment-guide)
36
+ - [Prerequisites](#prerequisites)
37
+ - [Installation](#installation)
38
+ - [Quick Deploy](#quick-deploy)
39
+ - [Deploy to Koyeb](#deploy-to-koyeb)
40
+ - [Deploy to Render](#deploy-to-render)
41
+ - [Deploy to Railway](#deploy-to-railway)
42
  - [Reverse Proxy Setup](#reverse-proxy-setup)
43
  - [Support & Community](#support--community)
44
  - [Troubleshooting & FAQ](#troubleshooting--faq)
 
98
  - 🎨 **Custom Templates** - Personalize messages sent by the bot to match your brand.
99
  - 📈 **Media Info Display** - Shows file size, duration, and format details in the response message.
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  ## Configuration
103
 
 
112
  | `BOT_TOKEN` | Bot token from @BotFather | `123456:ABCdefGHI` |
113
  | `BIN_CHANNEL` | Storage channel ID | `-1001234567890` |
114
  | `OWNER_ID` | Owner user ID | `12345678` |
 
115
  | `DATABASE_URL` | MongoDB connection | `mongodb+srv://...` |
116
  | `FQDN` | Domain/IP address | `f2l.thunder.com` |
117
  | `HAS_SSL` | HTTPS enabled | `True` or `False` |
118
  | `PORT` | Server port | `8080` |
119
  | `NO_PORT` | Hide port in URLs | `True` or `False` |
120
 
121
+ ### Optional Configuration
122
+
123
  <details>
124
+ <summary>Optional Configuration Details</summary>
125
 
126
  | Variable | Description | Default |
127
  | :--- | :--- | :--- |
 
269
 
270
  Features include download/upload speeds, latency measurements, and shareable result images for performance monitoring.
271
 
272
+ ## Deployment Guide
273
+
274
+ This section covers the complete setup process for deploying Thunder, from prerequisites to production deployment.
275
+
276
+ ### Prerequisites
277
+
278
+ | Requirement | Description | Source |
279
+ | :--- | :--- | :--- |
280
+ | Python 3.13 | Programming language | [python.org](https://python.org) |
281
+ | MongoDB | Database | [mongodb.com](https://mongodb.com) |
282
+ | Telegram API | API credentials | [my.telegram.org](https://my.telegram.org/apps) |
283
+ | Bot Token | From @BotFather | [@BotFather](https://t.me/BotFather) |
284
+ | Public Server | VPS/Dedicated server | Any provider |
285
+ | Storage Channel | For file storage | Create in Telegram |
286
+
287
+ ### Installation
288
 
289
+ #### Docker Installation (Recommended)
290
+
291
+ ```bash
292
+ # 1. Clone repository
293
+ git clone https://github.com/fyaz05/FileToLink.git
294
+ cd FileToLink
295
 
296
+ # 2. Configure
297
+ cp config_sample.env config.env
298
+ nano config.env # Edit your settings
299
 
300
+ # 3. Build and run
301
+ docker build -t thunder .
302
+ docker run -d --name thunder -p 8080:8080 thunder
303
+ ```
304
 
305
  <details>
306
+ <summary>Manual Installation</summary>
307
+
308
+ ```bash
309
+ # 1. Clone repository
310
+ git clone https://github.com/fyaz05/FileToLink.git
311
+ cd FileToLink
312
+
313
+ # 2. Setup virtual environment
314
+ python3 -m venv venv
315
+ source venv/bin/activate # Windows: venv\Scripts\activate
316
+
317
+ # 3. Install dependencies
318
+ pip install -r requirements.txt
319
+
320
+ # 4. Configure
321
+ cp config_sample.env config.env
322
+ nano config.env
323
+
324
+ # 5. Run bot
325
+ python -m Thunder
326
+ ```
327
+
328
+ > **Tip:** Start with the essential configuration to get Thunder running, then add optional features as needed.
329
+
330
+ </details>
331
+
332
+ ## Quick Deploy
333
+
334
+ ### Deploy to Koyeb
335
+
336
+ [![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=docker&image=docker.io/fyaz05/thunder:latest&name=thunder&ports=8080;http;/&env[API_ID]=&env[API_HASH]=&env[BOT_TOKEN]=&env[BIN_CHANNEL]=&env[OWNER_ID]=&env[DATABASE_URL]=&env[FQDN]=)
337
+
338
+
339
+ After deployment, to add any additional environment variables, use the Koyeb dashboard under **Settings** → **Environment Variables**.
340
+
341
+ ### Deploy to Render
342
+
343
+ 1. Open [Render Dashboard](https://dashboard.render.com) → **New** → **Web Service**
344
+ 2. Choose **Existing Image**: `fyaz05/thunder:latest`
345
+ 3. Add your environment variables
346
+ 4. Click **Deploy**
347
+
348
+ ### Deploy to Railway
349
+
350
+ 1. Open [Railway](https://railway.app) → **New Project** → **Deploy Service**
351
+ 2. Choose **Docker Image**: `fyaz05/thunder:latest`
352
+ 3. Add your environment variables
353
+ 4. Click **Deploy**
354
+
355
+ > **Note:** See the [Configuration](#configuration) section for required environment variables.
356
+
357
+ ## Reverse Proxy Setup
358
+
359
+ <details>
360
+ <summary>Reverse Proxy Guide</summary>
361
 
362
  This guide will help you set up a secure reverse proxy using **NGINX** for your file streaming bot with **Cloudflare SSL protection**.
363
 
 
454
 
455
  ### Troubleshooting & FAQ
456
 
457
+ #### **Initial Setup**
458
+
459
+ **Q: Why isn't my bot responding after setup?**
460
+ A: This is usually a configuration issue. Please check the following:
461
+ 1. **Verify `config.env`**: Make sure all essential variables (`API_ID`, `API_HASH`, `BOT_TOKEN`, `BIN_CHANNEL`, `DATABASE_URL`) are filled in correctly.
462
+ 2. **Use `config.env` Only**: Do not edit `vars.py` or `config_sample.env`. The bot is designed to only read your settings from `config.env`.
463
+ 3. **Check Logs**: Review the console logs on your server or hosting platform (Koyeb, Render, Heroku) for any startup errors.
464
+
465
+ **Q: What do I use for the `FQDN` variable?**
466
+ A: It's the public URL or IP address of your bot.
467
+ - **With a Domain**: Use your subdomain (e.g., `f2l.thunder.com`).
468
+ - **On Koyeb/Render/Heroku**: Use the public URL provided by the platform.
469
+ - **On a VPS**: Use your server's public IP address.
470
+
471
+ **Q: Why are my links not working on a VPS?**
472
+ A: For links to work on a VPS, the URL must include the port number (e.g., `http://YOUR_VPS_IP:8080`). Ensure that `NO_PORT` is set to `False` in `config.env` and that your server is configured to allow traffic through that port.
473
+
474
+ #### **Common Errors**
475
+
476
+ **Q: Why are my links showing a "Resource Not Found" error or not working?**
477
+ A: This error means the bot can't access the file. Check these three things:
478
+ 1. **Invalid Token**: Your `BOT_TOKEN` or one of the `MULTI_TOKEN`s might be wrong. Double-check them with @BotFather.
479
+ 2. **Missing Admin Rights**: The bot and **all** your client accounts must be **administrators** in the `BIN_CHANNEL`.
480
+ 3. **File Deleted**: The link will break if the file was deleted from your `BIN_CHANNEL`.
481
+
482
+ **Q: Why isn't video or audio playing correctly in my browser?**
483
+ A: Your browser likely doesn't support the file's audio or video format (codec). This is a browser limitation, not a bot issue.
484
+ - **Solution**: For perfect playback, copy the link and play it in a dedicated media player. Recommended players include **VLC Media Player**, **MX Player**, **PotPlayer**, **IINA**, and **MPV**.
485
+
486
+ **Q: Why does the bot sometimes become unresponsive?**
487
+ A: This is likely a **Telegram Flood Wait**. To prevent spam, Telegram temporarily limits accounts that make too many requests. The bot is designed to handle this automatically by pausing and will resume on its own once the limit is lifted.
488
 
489
+ #### **Performance**
 
 
490
 
491
+ **Q: How can I fix slow download and streaming speeds?**
492
+ A: If your speeds are slow, here’s how to fix it:
493
+ - **Add More Clients**: This is the best solution. Add `MULTI_TOKEN`s to your `config.env` to distribute the workload and increase throughput.
494
+ - **Use DC4 Accounts**: For top performance, use Telegram accounts from **Data Center 4 (DC4)**, as they often have the fastest connection. Use `/dc` to check an account's data center.
495
+ - **Upgrade Your Server**: A server with a slow network will bottleneck your speeds. Consider upgrading your VPS plan.
496
 
497
+ #### **Bot Usage**
 
 
498
 
499
+ **Q: How do I generate links for multiple files at once?**
500
+ A: The `/link` command can process multiple files sent in sequence. To use it, **reply to the first file** of the series with the command and the total count.
501
+ - **Example**: For a series of 5 files, reply to the very first file with `/link 5`.
502
 
503
+ **Q: Can I mix tokens from different accounts and data centers?**
504
+ A: Yes. Mixing clients from different accounts and data centers (like DC1, DC4, and DC5) is a great way to improve bot performance and reliability.
505
 
506
  ### Contributing
507
 
 
521
 
522
  - [Pyrofork](https://github.com/Mayuri-Chan/pyrofork) - Telegram MTProto API Framework
523
  - [aiohttp](https://github.com/aio-libs/aiohttp) - Asynchronous HTTP Client/Server
524
+ - [PyMongo](https://github.com/mongodb/mongo-python-driver) - Asynchronous MongoDB Driver
525
  - [TgCrypto](https://github.com/pyrogram/tgcrypto) - High-performance cryptography library
526
 
527
  ## ⚠️ Disclaimer
Thunder/__init__.py CHANGED
@@ -3,4 +3,4 @@
3
  import time
4
 
5
  StartTime = time.time()
6
- __version__ = "1.9.8"
 
3
  import time
4
 
5
  StartTime = time.time()
6
+ __version__ = "1.9.9"
Thunder/__main__.py CHANGED
@@ -12,6 +12,7 @@ from pathlib import Path
12
  install()
13
  from aiohttp import web
14
  from pyrogram import idle
 
15
 
16
  from Thunder import __version__
17
  from Thunder.bot import StreamBot
@@ -19,7 +20,6 @@ from Thunder.bot.clients import cleanup_clients, initialize_clients
19
  from Thunder.server import web_server
20
  from Thunder.utils.commands import set_commands
21
  from Thunder.utils.database import db
22
- from Thunder.utils.handler import handle_flood_wait
23
  from Thunder.utils.keepalive import ping_server
24
  from Thunder.utils.logger import logger
25
  from Thunder.utils.messages import MSG_ADMIN_RESTART_DONE
@@ -100,8 +100,20 @@ async def start_services():
100
 
101
  print(" ▶ Starting Telegram Bot initialization...")
102
  try:
103
- await handle_flood_wait(StreamBot.start)
104
- bot_info = await handle_flood_wait(StreamBot.get_me)
 
 
 
 
 
 
 
 
 
 
 
 
105
  StreamBot.username = bot_info.username
106
  print(f" ✓ Bot initialized successfully as @{StreamBot.username}")
107
 
@@ -111,12 +123,22 @@ async def start_services():
111
  restart_message_data = await db.get_restart_message()
112
  if restart_message_data:
113
  try:
114
- await handle_flood_wait(
115
- StreamBot.edit_message_text,
116
- chat_id=restart_message_data["chat_id"],
117
- message_id=restart_message_data["message_id"],
118
- text=MSG_ADMIN_RESTART_DONE,
119
- )
 
 
 
 
 
 
 
 
 
 
120
  await db.delete_restart_message(
121
  restart_message_data["message_id"]
122
  )
@@ -197,7 +219,6 @@ async def start_services():
197
  print(f" ▶ Bot Name: {bot_info.first_name}")
198
  print(f" ▶ Username: @{bot_info.username}")
199
  print(f" ▶ Server: {bind_address}:{Var.PORT}")
200
- print(f" ▶ Owner: {Var.OWNER_USERNAME}")
201
  print(f" ▶ Startup Time: {elapsed_time:.2f} seconds")
202
  print("╚═══════════════════════════════════════════════════════════╝")
203
  print(" ▶ Bot is now running! Press CTRL+C to stop.")
 
12
  install()
13
  from aiohttp import web
14
  from pyrogram import idle
15
+ from pyrogram.errors import FloodWait, MessageNotModified
16
 
17
  from Thunder import __version__
18
  from Thunder.bot import StreamBot
 
20
  from Thunder.server import web_server
21
  from Thunder.utils.commands import set_commands
22
  from Thunder.utils.database import db
 
23
  from Thunder.utils.keepalive import ping_server
24
  from Thunder.utils.logger import logger
25
  from Thunder.utils.messages import MSG_ADMIN_RESTART_DONE
 
100
 
101
  print(" ▶ Starting Telegram Bot initialization...")
102
  try:
103
+ try:
104
+ await StreamBot.start()
105
+ except FloodWait as e:
106
+ logger.debug(f"FloodWait in bot start, sleeping for {e.value}s")
107
+ await asyncio.sleep(e.value)
108
+ await StreamBot.start()
109
+
110
+ try:
111
+ bot_info = await StreamBot.get_me()
112
+ except FloodWait as e:
113
+ logger.debug(f"FloodWait in get_me, sleeping for {e.value}s")
114
+ await asyncio.sleep(e.value)
115
+ bot_info = await StreamBot.get_me()
116
+
117
  StreamBot.username = bot_info.username
118
  print(f" ✓ Bot initialized successfully as @{StreamBot.username}")
119
 
 
123
  restart_message_data = await db.get_restart_message()
124
  if restart_message_data:
125
  try:
126
+ try:
127
+ await StreamBot.edit_message_text(
128
+ chat_id=restart_message_data["chat_id"],
129
+ message_id=restart_message_data["message_id"],
130
+ text=MSG_ADMIN_RESTART_DONE,
131
+ )
132
+ except FloodWait as e:
133
+ logger.debug(f"FloodWait in restart message edit, sleeping for {e.value}s")
134
+ await asyncio.sleep(e.value)
135
+ await StreamBot.edit_message_text(
136
+ chat_id=restart_message_data["chat_id"],
137
+ message_id=restart_message_data["message_id"],
138
+ text=MSG_ADMIN_RESTART_DONE,
139
+ )
140
+ except MessageNotModified:
141
+ pass
142
  await db.delete_restart_message(
143
  restart_message_data["message_id"]
144
  )
 
219
  print(f" ▶ Bot Name: {bot_info.first_name}")
220
  print(f" ▶ Username: @{bot_info.username}")
221
  print(f" ▶ Server: {bind_address}:{Var.PORT}")
 
222
  print(f" ▶ Startup Time: {elapsed_time:.2f} seconds")
223
  print("╚═══════════════════════════════════════════════════════════╝")
224
  print(" ▶ Bot is now running! Press CTRL+C to stop.")
Thunder/bot/clients.py CHANGED
@@ -3,17 +3,21 @@
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
 
@@ -46,7 +50,11 @@ async def initialize_clients():
46
  max_concurrent_transmissions=1000,
47
  sleep_threshold=Var.SLEEP_THRESHOLD
48
  )
49
- await handle_flood_wait(client.start)
 
 
 
 
50
  work_loads[client_id] = 0
51
  print(f" ◎ Client ID {client_id} started")
52
  return client_id, client
 
3
  import asyncio
4
 
5
  from pyrogram import Client
6
+ from pyrogram.errors import FloodWait
7
 
8
  from Thunder.bot import StreamBot, multi_clients, work_loads
9
  from Thunder.utils.config_parser import TokenParser
 
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
+ try:
17
+ await client.stop()
18
+ except FloodWait as e:
19
+ await asyncio.sleep(e.value)
20
+ await client.stop()
21
  except Exception as e:
22
  logger.error(f"Error stopping client: {e}", exc_info=True)
23
 
 
50
  max_concurrent_transmissions=1000,
51
  sleep_threshold=Var.SLEEP_THRESHOLD
52
  )
53
+ try:
54
+ await client.start()
55
+ except FloodWait as e:
56
+ await asyncio.sleep(e.value)
57
+ await client.start()
58
  work_loads[client_id] = 0
59
  print(f" ◎ Client ID {client_id} started")
60
  return client_id, client
Thunder/bot/plugins/admin.py CHANGED
@@ -12,6 +12,7 @@ import psutil
12
  from pyrogram import filters
13
  from pyrogram.client import Client
14
  from pyrogram.enums import ParseMode
 
15
  from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
16
 
17
  from Thunder import StartTime, __version__
@@ -19,7 +20,6 @@ from Thunder.bot import StreamBot, multi_clients, work_loads
19
  from Thunder.utils.bot_utils import reply
20
  from Thunder.utils.broadcast import broadcast_message
21
  from Thunder.utils.database import db
22
- from Thunder.utils.handler import handle_flood_wait
23
  from Thunder.utils.human_readable import humanbytes
24
  from Thunder.utils.logger import LOG_FILE, logger
25
  from Thunder.utils.messages import (
@@ -156,9 +156,12 @@ async def send_logs(client: Client, message: Message):
156
  return
157
 
158
  try:
159
- await handle_flood_wait(
160
- message.reply_document, LOG_FILE,
161
- caption=MSG_LOG_FILE_CAPTION)
 
 
 
162
  except Exception as e:
163
  logger.error(f"Error sending log file: {e}", exc_info=True)
164
  await reply(message, text=MSG_ERROR_GENERIC)
@@ -246,7 +249,12 @@ async def ban_command(client: Client, message: Message):
246
  text += MSG_CHANNEL_BANNED_REASON_SUFFIX.format(reason=reason)
247
  await reply(message, text=text)
248
  try:
249
- await handle_flood_wait(client.leave_chat, target_id)
 
 
 
 
 
250
  except Exception as e:
251
  logger.warning(f"Could not leave banned channel {target_id}: {e}", exc_info=True)
252
  else:
@@ -260,7 +268,12 @@ async def ban_command(client: Client, message: Message):
260
  text += MSG_BAN_REASON_SUFFIX.format(reason=reason)
261
  await reply(message, text=text)
262
  try:
263
- await handle_flood_wait(client.send_message, target_id, MSG_USER_BANNED_NOTIFICATION)
 
 
 
 
 
264
  except Exception as e:
265
  logger.warning(f"Could not notify banned user {target_id}: {e}", exc_info=True)
266
 
@@ -288,7 +301,12 @@ async def unban_command(client: Client, message: Message):
288
  if await db.remove_banned_user(user_id=target_id):
289
  await reply(message, text=MSG_ADMIN_USER_UNBANNED.format(user_id=target_id))
290
  try:
291
- await handle_flood_wait(client.send_message, target_id, MSG_USER_UNBANNED_NOTIFICATION)
 
 
 
 
 
292
  except Exception as e:
293
  logger.warning(f"Could not notify unbanned user {target_id}: {e}", exc_info=True)
294
  else:
@@ -331,24 +349,45 @@ async def run_shell_command(client: Client, message: Message):
331
 
332
  output = output.strip() or MSG_SHELL_NO_OUTPUT
333
 
334
- await handle_flood_wait(status_msg.delete)
 
 
 
 
 
335
 
336
  if len(output) > 4096:
337
  file = BytesIO(output.encode())
338
  file.name = "shell_output.txt"
339
- await handle_flood_wait(
340
- message.reply_document, file,
341
- caption=MSG_SHELL_OUTPUT.format(
342
- command=html.escape(command)))
 
 
 
 
 
 
 
 
343
  else:
344
  await reply(message, text=output, parse_mode=ParseMode.HTML)
345
 
346
  except Exception as e:
347
  try:
348
- await handle_flood_wait(
349
- status_msg.edit_text,
350
- MSG_SHELL_ERROR.format(error=html.escape(str(e))),
351
- parse_mode=ParseMode.HTML)
 
 
 
 
 
 
 
 
352
  except Exception:
353
  await reply(
354
  message,
@@ -362,7 +401,14 @@ async def speedtest_command(client: Client, message: Message):
362
  try:
363
  result_dict, image_url = await run_speedtest()
364
  if result_dict is None:
365
- await handle_flood_wait(status_msg.edit_text, MSG_SPEEDTEST_ERROR)
 
 
 
 
 
 
 
366
  return
367
 
368
  result_text = _format_speedtest_result(result_dict)
@@ -370,7 +416,14 @@ async def speedtest_command(client: Client, message: Message):
370
  except Exception as e:
371
  logger.error(f"Error in speedtest_command: {e}", exc_info=True)
372
  try:
373
- await handle_flood_wait(status_msg.edit_text, MSG_SPEEDTEST_ERROR)
 
 
 
 
 
 
 
374
  except Exception:
375
  await reply(message, text=MSG_SPEEDTEST_ERROR)
376
 
@@ -403,10 +456,27 @@ def _format_speedtest_result(result_dict: dict) -> str:
403
 
404
  async def _send_result(message: Message, status_msg: Message, result_text: str, image_url: str):
405
  if image_url:
406
- await handle_flood_wait(message.reply_photo, image_url, caption=result_text, parse_mode=ParseMode.MARKDOWN)
407
- await handle_flood_wait(status_msg.delete)
 
 
 
 
 
 
 
 
 
 
408
  else:
409
- await handle_flood_wait(status_msg.edit_text, result_text, parse_mode=ParseMode.MARKDOWN)
 
 
 
 
 
 
 
410
 
411
 
412
  def _fmt(value, decimals: int = 2) -> str:
 
12
  from pyrogram import filters
13
  from pyrogram.client import Client
14
  from pyrogram.enums import ParseMode
15
+ from pyrogram.errors import FloodWait, MessageNotModified
16
  from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
17
 
18
  from Thunder import StartTime, __version__
 
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.human_readable import humanbytes
24
  from Thunder.utils.logger import LOG_FILE, logger
25
  from Thunder.utils.messages import (
 
156
  return
157
 
158
  try:
159
+ try:
160
+ await message.reply_document(LOG_FILE, caption=MSG_LOG_FILE_CAPTION)
161
+ except FloodWait as e:
162
+ logger.debug(f"FloodWait in log file sending, sleeping for {e.value}s")
163
+ await asyncio.sleep(e.value)
164
+ await message.reply_document(LOG_FILE, caption=MSG_LOG_FILE_CAPTION)
165
  except Exception as e:
166
  logger.error(f"Error sending log file: {e}", exc_info=True)
167
  await reply(message, text=MSG_ERROR_GENERIC)
 
249
  text += MSG_CHANNEL_BANNED_REASON_SUFFIX.format(reason=reason)
250
  await reply(message, text=text)
251
  try:
252
+ try:
253
+ await client.leave_chat(target_id)
254
+ except FloodWait as e:
255
+ logger.debug(f"FloodWait in leave_chat, sleeping for {e.value}s")
256
+ await asyncio.sleep(e.value)
257
+ await client.leave_chat(target_id)
258
  except Exception as e:
259
  logger.warning(f"Could not leave banned channel {target_id}: {e}", exc_info=True)
260
  else:
 
268
  text += MSG_BAN_REASON_SUFFIX.format(reason=reason)
269
  await reply(message, text=text)
270
  try:
271
+ try:
272
+ await client.send_message(target_id, MSG_USER_BANNED_NOTIFICATION)
273
+ except FloodWait as e:
274
+ logger.debug(f"FloodWait in ban notification, sleeping for {e.value}s")
275
+ await asyncio.sleep(e.value)
276
+ await client.send_message(target_id, MSG_USER_BANNED_NOTIFICATION)
277
  except Exception as e:
278
  logger.warning(f"Could not notify banned user {target_id}: {e}", exc_info=True)
279
 
 
301
  if await db.remove_banned_user(user_id=target_id):
302
  await reply(message, text=MSG_ADMIN_USER_UNBANNED.format(user_id=target_id))
303
  try:
304
+ try:
305
+ await client.send_message(target_id, MSG_USER_UNBANNED_NOTIFICATION)
306
+ except FloodWait as e:
307
+ logger.debug(f"FloodWait in unban notification, sleeping for {e.value}s")
308
+ await asyncio.sleep(e.value)
309
+ await client.send_message(target_id, MSG_USER_UNBANNED_NOTIFICATION)
310
  except Exception as e:
311
  logger.warning(f"Could not notify unbanned user {target_id}: {e}", exc_info=True)
312
  else:
 
349
 
350
  output = output.strip() or MSG_SHELL_NO_OUTPUT
351
 
352
+ try:
353
+ await status_msg.delete()
354
+ except FloodWait as e:
355
+ logger.debug(f"FloodWait in shell status message delete, sleeping for {e.value}s")
356
+ await asyncio.sleep(e.value)
357
+ await status_msg.delete()
358
 
359
  if len(output) > 4096:
360
  file = BytesIO(output.encode())
361
  file.name = "shell_output.txt"
362
+ try:
363
+ await message.reply_document(
364
+ file,
365
+ caption=MSG_SHELL_OUTPUT.format(
366
+ command=html.escape(command)))
367
+ except FloodWait as e:
368
+ logger.debug(f"FloodWait in shell output document, sleeping for {e.value}s")
369
+ await asyncio.sleep(e.value)
370
+ await message.reply_document(
371
+ file,
372
+ caption=MSG_SHELL_OUTPUT.format(
373
+ command=html.escape(command)))
374
  else:
375
  await reply(message, text=output, parse_mode=ParseMode.HTML)
376
 
377
  except Exception as e:
378
  try:
379
+ try:
380
+ await status_msg.edit_text(
381
+ MSG_SHELL_ERROR.format(error=html.escape(str(e))),
382
+ parse_mode=ParseMode.HTML)
383
+ except FloodWait as e:
384
+ logger.debug(f"FloodWait in shell error message edit, sleeping for {e.value}s")
385
+ await asyncio.sleep(e.value)
386
+ await status_msg.edit_text(
387
+ MSG_SHELL_ERROR.format(error=html.escape(str(e))),
388
+ parse_mode=ParseMode.HTML)
389
+ except MessageNotModified:
390
+ pass
391
  except Exception:
392
  await reply(
393
  message,
 
401
  try:
402
  result_dict, image_url = await run_speedtest()
403
  if result_dict is None:
404
+ try:
405
+ await status_msg.edit_text(MSG_SPEEDTEST_ERROR)
406
+ except FloodWait as e:
407
+ logger.debug(f"FloodWait in speedtest error edit, sleeping for {e.value}s")
408
+ await asyncio.sleep(e.value)
409
+ await status_msg.edit_text(MSG_SPEEDTEST_ERROR)
410
+ except MessageNotModified:
411
+ pass
412
  return
413
 
414
  result_text = _format_speedtest_result(result_dict)
 
416
  except Exception as e:
417
  logger.error(f"Error in speedtest_command: {e}", exc_info=True)
418
  try:
419
+ try:
420
+ await status_msg.edit_text(MSG_SPEEDTEST_ERROR)
421
+ except FloodWait as e:
422
+ logger.debug(f"FloodWait in speedtest exception error edit, sleeping for {e.value}s")
423
+ await asyncio.sleep(e.value)
424
+ await status_msg.edit_text(MSG_SPEEDTEST_ERROR)
425
+ except MessageNotModified:
426
+ pass
427
  except Exception:
428
  await reply(message, text=MSG_SPEEDTEST_ERROR)
429
 
 
456
 
457
  async def _send_result(message: Message, status_msg: Message, result_text: str, image_url: str):
458
  if image_url:
459
+ try:
460
+ await message.reply_photo(image_url, caption=result_text, parse_mode=ParseMode.MARKDOWN)
461
+ except FloodWait as e:
462
+ logger.debug(f"FloodWait in speedtest photo reply, sleeping for {e.value}s")
463
+ await asyncio.sleep(e.value)
464
+ await message.reply_photo(image_url, caption=result_text, parse_mode=ParseMode.MARKDOWN)
465
+ try:
466
+ await status_msg.delete()
467
+ except FloodWait as e:
468
+ logger.debug(f"FloodWait in speedtest status delete, sleeping for {e.value}s")
469
+ await asyncio.sleep(e.value)
470
+ await status_msg.delete()
471
  else:
472
+ try:
473
+ await status_msg.edit_text(result_text, parse_mode=ParseMode.MARKDOWN)
474
+ except FloodWait as e:
475
+ logger.debug(f"FloodWait in speedtest result edit, sleeping for {e.value}s")
476
+ await asyncio.sleep(e.value)
477
+ await status_msg.edit_text(result_text, parse_mode=ParseMode.MARKDOWN)
478
+ except MessageNotModified:
479
+ pass
480
 
481
 
482
  def _fmt(value, decimals: int = 2) -> str:
Thunder/bot/plugins/callbacks.py CHANGED
@@ -1,14 +1,15 @@
1
  # Thunder/bot/plugins/callbacks.py
2
 
 
 
3
  from pyrogram import Client, filters
4
- from pyrogram.errors import MessageNotModified, MessageDeleteForbidden
5
  from pyrogram.types import (CallbackQuery, InlineKeyboardButton,
6
  InlineKeyboardMarkup)
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
  MSG_ABOUT, MSG_BROADCAST_CANCEL, MSG_BUTTON_ABOUT, MSG_BUTTON_CLOSE,
@@ -22,7 +23,11 @@ async def get_force_channel_button(client: Client):
22
  if not Var.FORCE_CHANNEL_ID:
23
  return None
24
  try:
25
- chat = await handle_flood_wait(client.get_chat, Var.FORCE_CHANNEL_ID)
 
 
 
 
26
  if chat:
27
  invite_link = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
28
  if invite_link:
@@ -43,17 +48,28 @@ 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 handle_flood_wait(
47
- callback_query.message.edit_text,
48
- text=MSG_HELP.format(max_files=Var.MAX_BATCH_FILES),
49
- reply_markup=InlineKeyboardMarkup(buttons),
50
- disable_web_page_preview=True
51
- )
 
 
 
 
 
 
 
52
  except MessageNotModified:
53
  pass
54
  except Exception as e:
55
  logger.error(f"Error in help callback: {e}", exc_info=True)
56
- await handle_flood_wait(callback_query.answer, "An error occurred. Please try again.", show_alert=True)
 
 
 
 
57
 
58
  @StreamBot.on_callback_query(filters.regex(r"^about_command$"))
59
  async def about_callback(client: Client, callback_query: CallbackQuery):
@@ -66,46 +82,80 @@ async def about_callback(client: Client, callback_query: CallbackQuery):
66
  InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")
67
  ]
68
  ]
69
- await handle_flood_wait(
70
- callback_query.message.edit_text,
71
- text=MSG_ABOUT,
72
- reply_markup=InlineKeyboardMarkup(buttons),
73
- disable_web_page_preview=True
74
- )
 
 
 
 
 
 
 
75
  except MessageNotModified:
76
  pass
77
  except Exception as e:
78
  logger.error(f"Error in about callback: {e}", exc_info=True)
79
- await handle_flood_wait(callback_query.answer, "An error occurred. Please try again.", show_alert=True)
 
 
 
 
80
 
81
  @StreamBot.on_callback_query(filters.regex(r"^restart_broadcast$"))
82
  async def restart_broadcast_callback(client: Client, callback_query: CallbackQuery):
83
  if not await owner_only(client, callback_query):
84
  return
85
  try:
86
- await handle_flood_wait(callback_query.answer, MSG_ERROR_BROADCAST_RESTART, show_alert=True)
 
 
 
 
87
  buttons = [
88
  [
89
  InlineKeyboardButton(MSG_BUTTON_GET_HELP, callback_data="help_command"),
90
  InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")
91
  ]
92
  ]
93
- await handle_flood_wait(
94
- callback_query.message.edit_text,
95
- MSG_ERROR_BROADCAST_INSTRUCTION,
96
- reply_markup=InlineKeyboardMarkup(buttons),
97
- disable_web_page_preview=True
98
- )
 
 
 
 
 
 
 
99
  except Exception as e:
100
  logger.error(f"Error in restart broadcast callback: {e}", exc_info=True)
101
- await handle_flood_wait(callback_query.answer, "An error occurred. Please try again.", show_alert=True)
 
 
 
 
102
 
103
  @StreamBot.on_callback_query(filters.regex(r"^close_panel$"))
104
  async def close_panel_callback(client: Client, callback_query: CallbackQuery):
105
  try:
106
- await handle_flood_wait(callback_query.answer)
107
  try:
108
- await handle_flood_wait(callback_query.message.delete)
 
 
 
 
 
 
 
 
 
109
  except MessageDeleteForbidden:
110
  logger.debug(f"Failed to delete callback query message due to permissions. Message ID: {callback_query.message.id}")
111
  except Exception as e:
@@ -114,7 +164,11 @@ async def close_panel_callback(client: Client, callback_query: CallbackQuery):
114
  if callback_query.message.reply_to_message:
115
  try:
116
  reply_msg = callback_query.message.reply_to_message
117
- await handle_flood_wait(reply_msg.delete)
 
 
 
 
118
  except MessageDeleteForbidden:
119
  logger.debug(f"Failed to delete replied message due to permissions. Message ID: {reply_msg.id}")
120
  except Exception as e:
@@ -128,23 +182,42 @@ async def cancel_broadcast(client: Client, callback_query: CallbackQuery):
128
  broadcast_id = callback_query.data.split("_")[1]
129
  if broadcast_id in broadcast_ids:
130
  broadcast_ids[broadcast_id]["cancelled"] = True
131
- await handle_flood_wait(
132
- callback_query.message.edit_text,
133
- MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id)
134
- )
 
 
 
 
 
135
  else:
136
- await handle_flood_wait(
137
- callback_query.answer,
138
- MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id),
139
- show_alert=True
140
- )
 
 
 
 
 
 
141
  except Exception as e:
142
  logger.error(f"Error in cancel broadcast callback: {e}", exc_info=True)
143
- await handle_flood_wait(callback_query.answer, "An error occurred. Please try again.", show_alert=True)
 
 
 
 
144
 
145
  @StreamBot.on_callback_query()
146
  async def fallback_callback(client: Client, callback_query: CallbackQuery):
147
  try:
148
- await handle_flood_wait(callback_query.answer, MSG_ERROR_CALLBACK_UNSUPPORTED, show_alert=True)
 
 
 
 
149
  except Exception as e:
150
  logger.error(f"Error in fallback callback: {e}", exc_info=True)
 
1
  # Thunder/bot/plugins/callbacks.py
2
 
3
+ import asyncio
4
+
5
  from pyrogram import Client, filters
6
+ from pyrogram.errors import FloodWait, MessageNotModified, MessageDeleteForbidden
7
  from pyrogram.types import (CallbackQuery, InlineKeyboardButton,
8
  InlineKeyboardMarkup)
9
 
10
  from Thunder.bot import StreamBot
11
  from Thunder.utils.broadcast import broadcast_ids
12
  from Thunder.utils.decorators import owner_only
 
13
  from Thunder.utils.logger import logger
14
  from Thunder.utils.messages import (
15
  MSG_ABOUT, MSG_BROADCAST_CANCEL, MSG_BUTTON_ABOUT, MSG_BUTTON_CLOSE,
 
23
  if not Var.FORCE_CHANNEL_ID:
24
  return None
25
  try:
26
+ try:
27
+ chat = await client.get_chat(Var.FORCE_CHANNEL_ID)
28
+ except FloodWait as e:
29
+ await asyncio.sleep(e.value)
30
+ chat = await client.get_chat(Var.FORCE_CHANNEL_ID)
31
  if chat:
32
  invite_link = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
33
  if invite_link:
 
48
  if force_button:
49
  buttons.append(force_button)
50
  buttons.append([InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")])
51
+ try:
52
+ await callback_query.message.edit_text(
53
+ text=MSG_HELP.format(max_files=Var.MAX_BATCH_FILES),
54
+ reply_markup=InlineKeyboardMarkup(buttons),
55
+ disable_web_page_preview=True
56
+ )
57
+ except FloodWait as e:
58
+ await asyncio.sleep(e.value)
59
+ await callback_query.message.edit_text(
60
+ text=MSG_HELP.format(max_files=Var.MAX_BATCH_FILES),
61
+ reply_markup=InlineKeyboardMarkup(buttons),
62
+ disable_web_page_preview=True
63
+ )
64
  except MessageNotModified:
65
  pass
66
  except Exception as e:
67
  logger.error(f"Error in help callback: {e}", exc_info=True)
68
+ try:
69
+ await callback_query.answer("An error occurred. Please try again.", show_alert=True)
70
+ except FloodWait as e:
71
+ await asyncio.sleep(e.value)
72
+ await callback_query.answer("An error occurred. Please try again.", show_alert=True)
73
 
74
  @StreamBot.on_callback_query(filters.regex(r"^about_command$"))
75
  async def about_callback(client: Client, callback_query: CallbackQuery):
 
82
  InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")
83
  ]
84
  ]
85
+ try:
86
+ await callback_query.message.edit_text(
87
+ text=MSG_ABOUT,
88
+ reply_markup=InlineKeyboardMarkup(buttons),
89
+ disable_web_page_preview=True
90
+ )
91
+ except FloodWait as e:
92
+ await asyncio.sleep(e.value)
93
+ await callback_query.message.edit_text(
94
+ text=MSG_ABOUT,
95
+ reply_markup=InlineKeyboardMarkup(buttons),
96
+ disable_web_page_preview=True
97
+ )
98
  except MessageNotModified:
99
  pass
100
  except Exception as e:
101
  logger.error(f"Error in about callback: {e}", exc_info=True)
102
+ try:
103
+ await callback_query.answer("An error occurred. Please try again.", show_alert=True)
104
+ except FloodWait as e:
105
+ await asyncio.sleep(e.value)
106
+ await callback_query.answer("An error occurred. Please try again.", show_alert=True)
107
 
108
  @StreamBot.on_callback_query(filters.regex(r"^restart_broadcast$"))
109
  async def restart_broadcast_callback(client: Client, callback_query: CallbackQuery):
110
  if not await owner_only(client, callback_query):
111
  return
112
  try:
113
+ try:
114
+ await callback_query.answer(MSG_ERROR_BROADCAST_RESTART, show_alert=True)
115
+ except FloodWait as e:
116
+ await asyncio.sleep(e.value)
117
+ await callback_query.answer(MSG_ERROR_BROADCAST_RESTART, show_alert=True)
118
  buttons = [
119
  [
120
  InlineKeyboardButton(MSG_BUTTON_GET_HELP, callback_data="help_command"),
121
  InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")
122
  ]
123
  ]
124
+ try:
125
+ await callback_query.message.edit_text(
126
+ MSG_ERROR_BROADCAST_INSTRUCTION,
127
+ reply_markup=InlineKeyboardMarkup(buttons),
128
+ disable_web_page_preview=True
129
+ )
130
+ except FloodWait as e:
131
+ await asyncio.sleep(e.value)
132
+ await callback_query.message.edit_text(
133
+ MSG_ERROR_BROADCAST_INSTRUCTION,
134
+ reply_markup=InlineKeyboardMarkup(buttons),
135
+ disable_web_page_preview=True
136
+ )
137
  except Exception as e:
138
  logger.error(f"Error in restart broadcast callback: {e}", exc_info=True)
139
+ try:
140
+ await callback_query.answer("An error occurred. Please try again.", show_alert=True)
141
+ except FloodWait as e:
142
+ await asyncio.sleep(e.value)
143
+ await callback_query.answer("An error occurred. Please try again.", show_alert=True)
144
 
145
  @StreamBot.on_callback_query(filters.regex(r"^close_panel$"))
146
  async def close_panel_callback(client: Client, callback_query: CallbackQuery):
147
  try:
 
148
  try:
149
+ await callback_query.answer()
150
+ except FloodWait as e:
151
+ await asyncio.sleep(e.value)
152
+ await callback_query.answer()
153
+ try:
154
+ try:
155
+ await callback_query.message.delete()
156
+ except FloodWait as e:
157
+ await asyncio.sleep(e.value)
158
+ await callback_query.message.delete()
159
  except MessageDeleteForbidden:
160
  logger.debug(f"Failed to delete callback query message due to permissions. Message ID: {callback_query.message.id}")
161
  except Exception as e:
 
164
  if callback_query.message.reply_to_message:
165
  try:
166
  reply_msg = callback_query.message.reply_to_message
167
+ try:
168
+ await reply_msg.delete()
169
+ except FloodWait as e:
170
+ await asyncio.sleep(e.value)
171
+ await reply_msg.delete()
172
  except MessageDeleteForbidden:
173
  logger.debug(f"Failed to delete replied message due to permissions. Message ID: {reply_msg.id}")
174
  except Exception as e:
 
182
  broadcast_id = callback_query.data.split("_")[1]
183
  if broadcast_id in broadcast_ids:
184
  broadcast_ids[broadcast_id]["cancelled"] = True
185
+ try:
186
+ await callback_query.message.edit_text(
187
+ MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id)
188
+ )
189
+ except FloodWait as e:
190
+ await asyncio.sleep(e.value)
191
+ await callback_query.message.edit_text(
192
+ MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id)
193
+ )
194
  else:
195
+ try:
196
+ await callback_query.answer(
197
+ MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id),
198
+ show_alert=True
199
+ )
200
+ except FloodWait as e:
201
+ await asyncio.sleep(e.value)
202
+ await callback_query.answer(
203
+ MSG_BROADCAST_CANCEL.format(broadcast_id=broadcast_id),
204
+ show_alert=True
205
+ )
206
  except Exception as e:
207
  logger.error(f"Error in cancel broadcast callback: {e}", exc_info=True)
208
+ try:
209
+ await callback_query.answer("An error occurred. Please try again.", show_alert=True)
210
+ except FloodWait as e:
211
+ await asyncio.sleep(e.value)
212
+ await callback_query.answer("An error occurred. Please try again.", show_alert=True)
213
 
214
  @StreamBot.on_callback_query()
215
  async def fallback_callback(client: Client, callback_query: CallbackQuery):
216
  try:
217
+ try:
218
+ await callback_query.answer(MSG_ERROR_CALLBACK_UNSUPPORTED, show_alert=True)
219
+ except FloodWait as e:
220
+ await asyncio.sleep(e.value)
221
+ await callback_query.answer(MSG_ERROR_CALLBACK_UNSUPPORTED, show_alert=True)
222
  except Exception as e:
223
  logger.error(f"Error in fallback callback: {e}", exc_info=True)
Thunder/bot/plugins/common.py CHANGED
@@ -1,9 +1,11 @@
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
  Message, User)
9
 
@@ -14,7 +16,6 @@ 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 (
@@ -47,16 +48,30 @@ async def start_command(bot: Client, msg: Message):
47
  token = await db.token_col.find_one({"token": payload})
48
  if token:
49
  if token["user_id"] != user.id:
50
- return await handle_flood_wait(msg.reply_text, text=MSG_TOKEN_FAILED.format(
51
- reason="This activation link is not for your account.",
52
- error_id=str(int(time.time()))[-8:]
53
- ))
 
 
 
 
 
 
 
54
 
55
  if token.get("activated"):
56
- return await handle_flood_wait(msg.reply_text, text=MSG_TOKEN_FAILED.format(
57
- reason="Token has already been activated.",
58
- error_id=str(int(time.time()))[-8:]
59
- ))
 
 
 
 
 
 
 
60
 
61
  now = datetime.utcnow()
62
  exp = now + timedelta(hours=Var.TOKEN_TTL_HOURS)
@@ -67,9 +82,17 @@ async def start_command(bot: Client, msg: Message):
67
  )
68
 
69
  hrs = round((exp - now).total_seconds() / 3600, 1)
70
- return await handle_flood_wait(msg.reply_text, text=MSG_TOKEN_ACTIVATED.format(duration_hours=hrs))
 
 
 
 
71
  else:
72
- return await handle_flood_wait(msg.reply_text, text=MSG_TOKEN_INVALID)
 
 
 
 
73
 
74
  txt = MSG_WELCOME.format(user_name=user.first_name if user else "Unknown")
75
  link, title = await get_force_info(bot)
@@ -86,7 +109,11 @@ async def start_command(bot: Client, msg: Message):
86
  if link:
87
  btns.append([InlineKeyboardButton(MSG_BUTTON_JOIN_CHANNEL.format(channel_title=title), url=link)])
88
 
89
- await handle_flood_wait(msg.reply_text, text=txt, reply_markup=InlineKeyboardMarkup(btns))
 
 
 
 
90
 
91
  @StreamBot.on_message(filters.command("help") & filters.private)
92
  async def help_command(bot: Client, msg: Message):
@@ -103,7 +130,11 @@ async def help_command(bot: Client, msg: Message):
103
  btns.append([InlineKeyboardButton(MSG_BUTTON_JOIN_CHANNEL.format(channel_title=title), url=link)])
104
 
105
  btns.append([InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")])
106
- await handle_flood_wait(msg.reply_text, text=txt, reply_markup=InlineKeyboardMarkup(btns))
 
 
 
 
107
 
108
  @StreamBot.on_message(filters.command("about") & filters.private)
109
  async def about_command(bot: Client, msg: Message):
@@ -118,7 +149,11 @@ async def about_command(bot: Client, msg: Message):
118
  InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
119
  ]
120
 
121
- await handle_flood_wait(msg.reply_text, text=MSG_ABOUT, reply_markup=InlineKeyboardMarkup(btns))
 
 
 
 
122
 
123
  async def send_user_dc(msg: Message, user: User):
124
  txt = await gen_dc_txt(user)
@@ -127,7 +162,11 @@ async def send_user_dc(msg: Message, user: User):
127
  [InlineKeyboardButton(MSG_BUTTON_VIEW_PROFILE, url=url)],
128
  [InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
129
  ]
130
- await handle_flood_wait(msg.reply_text, text=txt, reply_markup=InlineKeyboardMarkup(btns))
 
 
 
 
131
 
132
  async def send_file_dc(msg: Message, file_msg: Message):
133
  try:
@@ -161,7 +200,11 @@ async def send_file_dc(msg: Message, file_msg: Message):
161
  )
162
 
163
  btns = [[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]
164
- await handle_flood_wait(msg.reply_text, text=txt, reply_markup=InlineKeyboardMarkup(btns))
 
 
 
 
165
 
166
  except Exception as e:
167
  logger.error(f"File DC error: {e}", exc_info=True)
@@ -207,7 +250,11 @@ async def ping_command(bot: Client, msg: Message):
207
  if not await force_channel_check(bot, msg):
208
  return
209
  start = time.time()
210
- sent = await handle_flood_wait(msg.reply_text, text=MSG_PING_START)
 
 
 
 
211
  end = time.time()
212
  ms = (end - start) * 1000
213
 
@@ -216,8 +263,18 @@ async def ping_command(bot: Client, msg: Message):
216
  InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
217
  ]
218
 
219
- await handle_flood_wait(sent.edit_text,
220
- MSG_PING_RESPONSE.format(time_taken_ms=ms),
221
- reply_markup=InlineKeyboardMarkup(btns),
222
- disable_web_page_preview=True
223
- )
 
 
 
 
 
 
 
 
 
 
 
1
  # Thunder/bot/plugins/common.py
2
 
3
+ import asyncio
4
  import time
5
  from datetime import datetime, timedelta
6
 
7
  from pyrogram import Client, filters
8
+ from pyrogram.errors import FloodWait, MessageNotModified
9
  from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
10
  Message, User)
11
 
 
16
  from Thunder.utils.decorators import check_banned
17
  from Thunder.utils.file_properties import get_fname, get_fsize, parse_fid
18
  from Thunder.utils.force_channel import force_channel_check, get_force_info
 
19
  from Thunder.utils.human_readable import humanbytes
20
  from Thunder.utils.logger import logger
21
  from Thunder.utils.messages import (
 
48
  token = await db.token_col.find_one({"token": payload})
49
  if token:
50
  if token["user_id"] != user.id:
51
+ try:
52
+ return await msg.reply_text(text=MSG_TOKEN_FAILED.format(
53
+ reason="This activation link is not for your account.",
54
+ error_id=str(int(time.time()))[-8:]
55
+ ))
56
+ except FloodWait as e:
57
+ await asyncio.sleep(e.value)
58
+ return await msg.reply_text(text=MSG_TOKEN_FAILED.format(
59
+ reason="This activation link is not for your account.",
60
+ error_id=str(int(time.time()))[-8:]
61
+ ))
62
 
63
  if token.get("activated"):
64
+ try:
65
+ return await msg.reply_text(text=MSG_TOKEN_FAILED.format(
66
+ reason="Token has already been activated.",
67
+ error_id=str(int(time.time()))[-8:]
68
+ ))
69
+ except FloodWait as e:
70
+ await asyncio.sleep(e.value)
71
+ return await msg.reply_text(text=MSG_TOKEN_FAILED.format(
72
+ reason="Token has already been activated.",
73
+ error_id=str(int(time.time()))[-8:]
74
+ ))
75
 
76
  now = datetime.utcnow()
77
  exp = now + timedelta(hours=Var.TOKEN_TTL_HOURS)
 
82
  )
83
 
84
  hrs = round((exp - now).total_seconds() / 3600, 1)
85
+ try:
86
+ return await msg.reply_text(text=MSG_TOKEN_ACTIVATED.format(duration_hours=hrs))
87
+ except FloodWait as e:
88
+ await asyncio.sleep(e.value)
89
+ return await msg.reply_text(text=MSG_TOKEN_ACTIVATED.format(duration_hours=hrs))
90
  else:
91
+ try:
92
+ return await msg.reply_text(text=MSG_TOKEN_INVALID)
93
+ except FloodWait as e:
94
+ await asyncio.sleep(e.value)
95
+ return await msg.reply_text(text=MSG_TOKEN_INVALID)
96
 
97
  txt = MSG_WELCOME.format(user_name=user.first_name if user else "Unknown")
98
  link, title = await get_force_info(bot)
 
109
  if link:
110
  btns.append([InlineKeyboardButton(MSG_BUTTON_JOIN_CHANNEL.format(channel_title=title), url=link)])
111
 
112
+ try:
113
+ await msg.reply_text(text=txt, reply_markup=InlineKeyboardMarkup(btns))
114
+ except FloodWait as e:
115
+ await asyncio.sleep(e.value)
116
+ await msg.reply_text(text=txt, reply_markup=InlineKeyboardMarkup(btns))
117
 
118
  @StreamBot.on_message(filters.command("help") & filters.private)
119
  async def help_command(bot: Client, msg: Message):
 
130
  btns.append([InlineKeyboardButton(MSG_BUTTON_JOIN_CHANNEL.format(channel_title=title), url=link)])
131
 
132
  btns.append([InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")])
133
+ try:
134
+ await msg.reply_text(text=txt, reply_markup=InlineKeyboardMarkup(btns))
135
+ except FloodWait as e:
136
+ await asyncio.sleep(e.value)
137
+ await msg.reply_text(text=txt, reply_markup=InlineKeyboardMarkup(btns))
138
 
139
  @StreamBot.on_message(filters.command("about") & filters.private)
140
  async def about_command(bot: Client, msg: Message):
 
149
  InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
150
  ]
151
 
152
+ try:
153
+ await msg.reply_text(text=MSG_ABOUT, reply_markup=InlineKeyboardMarkup(btns))
154
+ except FloodWait as e:
155
+ await asyncio.sleep(e.value)
156
+ await msg.reply_text(text=MSG_ABOUT, reply_markup=InlineKeyboardMarkup(btns))
157
 
158
  async def send_user_dc(msg: Message, user: User):
159
  txt = await gen_dc_txt(user)
 
162
  [InlineKeyboardButton(MSG_BUTTON_VIEW_PROFILE, url=url)],
163
  [InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
164
  ]
165
+ try:
166
+ await msg.reply_text(text=txt, reply_markup=InlineKeyboardMarkup(btns))
167
+ except FloodWait as e:
168
+ await asyncio.sleep(e.value)
169
+ await msg.reply_text(text=txt, reply_markup=InlineKeyboardMarkup(btns))
170
 
171
  async def send_file_dc(msg: Message, file_msg: Message):
172
  try:
 
200
  )
201
 
202
  btns = [[InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]]
203
+ try:
204
+ await msg.reply_text(text=txt, reply_markup=InlineKeyboardMarkup(btns))
205
+ except FloodWait as e:
206
+ await asyncio.sleep(e.value)
207
+ await msg.reply_text(text=txt, reply_markup=InlineKeyboardMarkup(btns))
208
 
209
  except Exception as e:
210
  logger.error(f"File DC error: {e}", exc_info=True)
 
250
  if not await force_channel_check(bot, msg):
251
  return
252
  start = time.time()
253
+ try:
254
+ sent = await msg.reply_text(text=MSG_PING_START)
255
+ except FloodWait as e:
256
+ await asyncio.sleep(e.value)
257
+ sent = await msg.reply_text(text=MSG_PING_START)
258
  end = time.time()
259
  ms = (end - start) * 1000
260
 
 
263
  InlineKeyboardButton(MSG_BUTTON_CLOSE, callback_data="close_panel")]
264
  ]
265
 
266
+ try:
267
+ await sent.edit_text(
268
+ MSG_PING_RESPONSE.format(time_taken_ms=ms),
269
+ reply_markup=InlineKeyboardMarkup(btns),
270
+ disable_web_page_preview=True
271
+ )
272
+ except FloodWait as e:
273
+ await asyncio.sleep(e.value)
274
+ await sent.edit_text(
275
+ MSG_PING_RESPONSE.format(time_taken_ms=ms),
276
+ reply_markup=InlineKeyboardMarkup(btns),
277
+ disable_web_page_preview=True
278
+ )
279
+ except MessageNotModified:
280
+ pass
Thunder/bot/plugins/stream.py CHANGED
@@ -5,7 +5,7 @@ import secrets
5
  from typing import Any, Dict, Optional
6
 
7
  from pyrogram import Client, enums, filters
8
- from pyrogram.errors import MessageNotModified, MessageDeleteForbidden, MessageIdInvalid
9
  from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
10
  Message)
11
 
@@ -16,7 +16,6 @@ from Thunder.utils.database import db
16
  from Thunder.utils.decorators import (check_banned, get_shortener_status,
17
  require_token)
18
  from Thunder.utils.force_channel import force_channel_check
19
- from Thunder.utils.handler import handle_flood_wait
20
  from Thunder.utils.logger import logger
21
  from Thunder.utils.messages import (
22
  MSG_BATCH_LINKS_READY, MSG_BUTTON_DOWNLOAD, MSG_BUTTON_START_CHAT,
@@ -39,11 +38,19 @@ MESSAGE_DELAY = 0.5
39
 
40
  async def fwd_media(m_msg: Message) -> Optional[Message]:
41
  try:
42
- return await handle_flood_wait(m_msg.copy, chat_id=Var.BIN_CHANNEL)
 
 
 
 
43
  except Exception as e:
44
  if "MEDIA_CAPTION_TOO_LONG" in str(e):
45
  logger.debug(f"MEDIA_CAPTION_TOO_LONG error, retrying without caption: {e}")
46
- return await handle_flood_wait(m_msg.copy, chat_id=Var.BIN_CHANNEL, caption=None)
 
 
 
 
47
  logger.error(f"Error fwd_media copy: {e}", exc_info=True)
48
  return None
49
 
@@ -65,22 +72,38 @@ async def validate_request_common(client: Client, message: Message) -> Optional[
65
 
66
 
67
  async def send_channel_links(target_msg: Message, links: Dict[str, Any], source_info: str, source_id: int):
68
- await handle_flood_wait(
69
- target_msg.reply_text,
70
- MSG_NEW_FILE_REQUEST.format(
71
- source_info=source_info,
72
- id_=source_id,
73
- online_link=links['online_link'],
74
- stream_link=links['stream_link']
75
- ),
76
- disable_web_page_preview=True,
77
- quote=True
78
- )
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
 
81
  async def safe_edit_message(message: Message, text: str, **kwargs):
82
  try:
83
- return await handle_flood_wait(message.edit_text, text, **kwargs)
 
 
 
 
84
  except MessageNotModified:
85
  pass
86
  except MessageDeleteForbidden:
@@ -91,7 +114,11 @@ async def safe_edit_message(message: Message, text: str, **kwargs):
91
 
92
  async def safe_delete_message(message: Message):
93
  try:
94
- await handle_flood_wait(message.delete)
 
 
 
 
95
  except MessageDeleteForbidden:
96
  logger.debug(f"Failed to delete message {message.id} due to permissions.")
97
  except Exception as e:
@@ -107,32 +134,55 @@ async def send_dm_links(bot: Client, user_id: int, links: Dict[str, Any], chat_t
107
  download_link=links['online_link'],
108
  stream_link=links['stream_link']
109
  )
110
- await handle_flood_wait(
111
- bot.send_message,
112
- chat_id=user_id,
113
- text=dm_text,
114
- disable_web_page_preview=True,
115
- parse_mode=enums.ParseMode.MARKDOWN,
116
- reply_markup=get_link_buttons(links)
117
- )
 
 
 
 
 
 
 
 
 
118
  except Exception as e:
119
  logger.error(f"Error sending DM to user {user_id}: {e}", exc_info=True)
120
 
121
 
122
  async def send_link(msg: Message, links: Dict[str, Any]):
123
- await handle_flood_wait(
124
- msg.reply_text,
125
- MSG_LINKS.format(
126
- file_name=links['media_name'],
127
- file_size=links['media_size'],
128
- download_link=links['online_link'],
129
- stream_link=links['stream_link']
130
- ),
131
- quote=True,
132
- parse_mode=enums.ParseMode.MARKDOWN,
133
- disable_web_page_preview=True,
134
- reply_markup=get_link_buttons(links)
135
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
 
138
  @StreamBot.on_message(filters.command("link") & ~filters.private)
@@ -143,14 +193,23 @@ async def link_handler(bot: Client, msg: Message, **kwargs):
143
  return
144
  if message.from_user and not await db.is_user_exist(message.from_user.id):
145
  invite_link = f"https://t.me/{client.me.username}?start=start"
146
- await handle_flood_wait(
147
- message.reply_text,
148
- MSG_ERROR_START_BOT.format(invite_link=invite_link),
149
- disable_web_page_preview=True,
150
- parse_mode=enums.ParseMode.MARKDOWN,
151
- reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_START_CHAT, url=invite_link)]]),
152
- quote=True
153
- )
 
 
 
 
 
 
 
 
 
154
  return
155
 
156
  if (message.chat.type in [enums.ChatType.GROUP, enums.ChatType.SUPERGROUP]
@@ -180,7 +239,11 @@ async def link_handler(bot: Client, msg: Message, **kwargs):
180
  await reply_user_err(message, MSG_ERROR_INVALID_NUMBER)
181
  return
182
 
183
- status_msg = await handle_flood_wait(message.reply_text, MSG_PROCESSING_REQUEST, quote=True)
 
 
 
 
184
  shortener_val = handler_kwargs.get('shortener', shortener_val)
185
  if num_files == 1:
186
  await process_single(client, message, message.reply_to_message, status_msg, shortener_val, notification_msg=notification_msg)
@@ -208,7 +271,11 @@ async def private_receive_handler(bot: Client, msg: Message, **kwargs):
208
  notification_msg = handler_kwargs.get('notification_msg')
209
 
210
  await log_newusr(client, message.from_user.id, message.from_user.first_name or "")
211
- status_msg = await handle_flood_wait(message.reply_text, MSG_PROCESSING_FILE, quote=True)
 
 
 
 
212
  await process_single(client, message, message, status_msg, shortener_val, notification_msg=notification_msg)
213
 
214
  await handle_rate_limited_request(bot, msg, _actual_private_receive_handler, **kwargs)
@@ -232,7 +299,11 @@ async def channel_receive_handler(bot: Client, msg: Message):
232
 
233
  if is_banned_statically or is_banned_dynamically:
234
  try:
235
- await handle_flood_wait(client.leave_chat, message.chat.id)
 
 
 
 
236
  except Exception as e:
237
  logger.error(f"Error leaving banned channel {message.chat.id}: {e}")
238
  return
@@ -254,16 +325,27 @@ async def channel_receive_handler(bot: Client, msg: Message):
254
 
255
  if notification_msg:
256
  try:
257
- await handle_flood_wait(
258
- notification_msg.edit_text,
259
- MSG_NEW_FILE_REQUEST.format(
260
- source_info=source_info,
261
- id_=message.chat.id,
262
- online_link=links['online_link'],
263
- stream_link=links['stream_link']
264
- ),
265
- disable_web_page_preview=True
266
- )
 
 
 
 
 
 
 
 
 
 
 
267
  except Exception as e:
268
  logger.error(f"Error editing notification message with links: {e}", exc_info=True)
269
  await send_channel_links(stored_msg, links, source_info, message.chat.id)
@@ -271,7 +353,11 @@ async def channel_receive_handler(bot: Client, msg: Message):
271
  await send_channel_links(stored_msg, links, source_info, message.chat.id)
272
 
273
  try:
274
- await handle_flood_wait(message.edit_reply_markup, reply_markup=get_link_buttons(links))
 
 
 
 
275
  except (MessageNotModified, MessageDeleteForbidden, MessageIdInvalid):
276
  logger.debug(f"Failed to edit reply markup for message {message.id} due to not modified, permissions or invalid ID. Sending new link instead.")
277
  await send_link(message, links)
@@ -339,17 +425,29 @@ async def process_single(
339
  source_info = source_msg.chat.title or "Unknown Channel"
340
  source_id = source_msg.chat.id
341
  if source_info and source_id:
342
- await handle_flood_wait(
343
- stored_msg.reply_text,
344
- MSG_NEW_FILE_REQUEST.format(
345
- source_info=source_info,
346
- id_=source_id,
347
- online_link=links['online_link'],
348
- stream_link=links['stream_link']
349
- ),
350
- disable_web_page_preview=True,
351
- quote=True
352
- )
 
 
 
 
 
 
 
 
 
 
 
 
353
  if status_msg:
354
  await safe_delete_message(status_msg)
355
  return links
@@ -381,18 +479,31 @@ async def process_batch(
381
  batch_size = min(BATCH_SIZE, count - batch_start)
382
  batch_ids = list(range(start_id + batch_start, start_id + batch_start + batch_size))
383
  try:
384
- await handle_flood_wait(
385
- status_msg.edit_text,
386
- MSG_PROCESSING_BATCH.format(
387
- batch_number=(batch_start // BATCH_SIZE) + 1,
388
- total_batches=(count + BATCH_SIZE - 1) // BATCH_SIZE,
389
- file_count=batch_size
 
 
 
 
 
 
 
 
 
 
390
  )
391
- )
392
  except MessageNotModified:
393
  pass
394
  try:
395
- messages = await handle_flood_wait(bot.get_messages, msg.chat.id, batch_ids)
 
 
 
 
396
  if messages is None:
397
  messages = []
398
  except Exception as e:
@@ -410,47 +521,81 @@ async def process_batch(
410
  failed += 1
411
  if (processed + failed) % BATCH_UPDATE_INTERVAL == 0 or (processed + failed) == count:
412
  try:
413
- await handle_flood_wait(
414
- status_msg.edit_text,
415
- MSG_PROCESSING_STATUS.format(
416
- processed=processed,
417
- total=count,
418
- failed=failed
 
 
 
 
 
 
 
 
 
 
419
  )
420
- )
421
  except MessageNotModified:
422
  pass
423
  for i in range(0, len(links_list), LINK_CHUNK_SIZE):
424
  chunk = links_list[i:i+LINK_CHUNK_SIZE]
425
  chunk_text = MSG_BATCH_LINKS_READY.format(count=len(chunk)) + f"\n\n`{chr(10).join(chunk)}`"
426
- await handle_flood_wait(
427
- msg.reply_text,
428
- chunk_text,
429
- quote=True,
430
- disable_web_page_preview=True,
431
- parse_mode=enums.ParseMode.MARKDOWN
432
- )
 
 
 
 
 
 
 
 
433
  if msg.chat.type != enums.ChatType.PRIVATE and msg.from_user:
434
  try:
435
- await handle_flood_wait(
436
- bot.send_message,
437
- chat_id=msg.from_user.id,
438
- text=MSG_DM_BATCH_PREFIX.format(chat_title=msg.chat.title or "the chat") + "\n" + chunk_text,
439
- disable_web_page_preview=True,
440
- parse_mode=enums.ParseMode.MARKDOWN
441
- )
 
 
 
 
 
 
 
 
442
  except Exception as e:
443
  logger.error(f"Error sending DM in batch: {e}", exc_info=True)
444
  await reply_user_err(msg, MSG_ERROR_DM_FAILED)
445
  if i + LINK_CHUNK_SIZE < len(links_list):
446
  await asyncio.sleep(MESSAGE_DELAY)
447
- await handle_flood_wait(
448
- status_msg.edit_text,
449
- MSG_PROCESSING_RESULT.format(
450
- processed=processed,
451
- total=count,
452
- failed=failed
 
 
 
 
 
 
 
 
 
 
453
  )
454
- )
455
  if notification_msg:
456
  await safe_delete_message(notification_msg)
 
5
  from typing import Any, Dict, Optional
6
 
7
  from pyrogram import Client, enums, filters
8
+ from pyrogram.errors import FloodWait, MessageNotModified, MessageDeleteForbidden, MessageIdInvalid
9
  from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
10
  Message)
11
 
 
16
  from Thunder.utils.decorators import (check_banned, get_shortener_status,
17
  require_token)
18
  from Thunder.utils.force_channel import force_channel_check
 
19
  from Thunder.utils.logger import logger
20
  from Thunder.utils.messages import (
21
  MSG_BATCH_LINKS_READY, MSG_BUTTON_DOWNLOAD, MSG_BUTTON_START_CHAT,
 
38
 
39
  async def fwd_media(m_msg: Message) -> Optional[Message]:
40
  try:
41
+ try:
42
+ return await m_msg.copy(chat_id=Var.BIN_CHANNEL)
43
+ except FloodWait as e:
44
+ await asyncio.sleep(e.value)
45
+ return await m_msg.copy(chat_id=Var.BIN_CHANNEL)
46
  except Exception as e:
47
  if "MEDIA_CAPTION_TOO_LONG" in str(e):
48
  logger.debug(f"MEDIA_CAPTION_TOO_LONG error, retrying without caption: {e}")
49
+ try:
50
+ return await m_msg.copy(chat_id=Var.BIN_CHANNEL, caption=None)
51
+ except FloodWait as e:
52
+ await asyncio.sleep(e.value)
53
+ return await m_msg.copy(chat_id=Var.BIN_CHANNEL, caption=None)
54
  logger.error(f"Error fwd_media copy: {e}", exc_info=True)
55
  return None
56
 
 
72
 
73
 
74
  async def send_channel_links(target_msg: Message, links: Dict[str, Any], source_info: str, source_id: int):
75
+ try:
76
+ await target_msg.reply_text(
77
+ MSG_NEW_FILE_REQUEST.format(
78
+ source_info=source_info,
79
+ id_=source_id,
80
+ online_link=links['online_link'],
81
+ stream_link=links['stream_link']
82
+ ),
83
+ disable_web_page_preview=True,
84
+ quote=True
85
+ )
86
+ except FloodWait as e:
87
+ await asyncio.sleep(e.value)
88
+ await target_msg.reply_text(
89
+ MSG_NEW_FILE_REQUEST.format(
90
+ source_info=source_info,
91
+ id_=source_id,
92
+ online_link=links['online_link'],
93
+ stream_link=links['stream_link']
94
+ ),
95
+ disable_web_page_preview=True,
96
+ quote=True
97
+ )
98
 
99
 
100
  async def safe_edit_message(message: Message, text: str, **kwargs):
101
  try:
102
+ try:
103
+ return await message.edit_text(text, **kwargs)
104
+ except FloodWait as e:
105
+ await asyncio.sleep(e.value)
106
+ return await message.edit_text(text, **kwargs)
107
  except MessageNotModified:
108
  pass
109
  except MessageDeleteForbidden:
 
114
 
115
  async def safe_delete_message(message: Message):
116
  try:
117
+ try:
118
+ await message.delete()
119
+ except FloodWait as e:
120
+ await asyncio.sleep(e.value)
121
+ await message.delete()
122
  except MessageDeleteForbidden:
123
  logger.debug(f"Failed to delete message {message.id} due to permissions.")
124
  except Exception as e:
 
134
  download_link=links['online_link'],
135
  stream_link=links['stream_link']
136
  )
137
+ try:
138
+ await bot.send_message(
139
+ chat_id=user_id,
140
+ text=dm_text,
141
+ disable_web_page_preview=True,
142
+ parse_mode=enums.ParseMode.MARKDOWN,
143
+ reply_markup=get_link_buttons(links)
144
+ )
145
+ except FloodWait as e:
146
+ await asyncio.sleep(e.value)
147
+ await bot.send_message(
148
+ chat_id=user_id,
149
+ text=dm_text,
150
+ disable_web_page_preview=True,
151
+ parse_mode=enums.ParseMode.MARKDOWN,
152
+ reply_markup=get_link_buttons(links)
153
+ )
154
  except Exception as e:
155
  logger.error(f"Error sending DM to user {user_id}: {e}", exc_info=True)
156
 
157
 
158
  async def send_link(msg: Message, links: Dict[str, Any]):
159
+ try:
160
+ await msg.reply_text(
161
+ MSG_LINKS.format(
162
+ file_name=links['media_name'],
163
+ file_size=links['media_size'],
164
+ download_link=links['online_link'],
165
+ stream_link=links['stream_link']
166
+ ),
167
+ quote=True,
168
+ parse_mode=enums.ParseMode.MARKDOWN,
169
+ disable_web_page_preview=True,
170
+ reply_markup=get_link_buttons(links)
171
+ )
172
+ except FloodWait as e:
173
+ await asyncio.sleep(e.value)
174
+ await msg.reply_text(
175
+ MSG_LINKS.format(
176
+ file_name=links['media_name'],
177
+ file_size=links['media_size'],
178
+ download_link=links['online_link'],
179
+ stream_link=links['stream_link']
180
+ ),
181
+ quote=True,
182
+ parse_mode=enums.ParseMode.MARKDOWN,
183
+ disable_web_page_preview=True,
184
+ reply_markup=get_link_buttons(links)
185
+ )
186
 
187
 
188
  @StreamBot.on_message(filters.command("link") & ~filters.private)
 
193
  return
194
  if message.from_user and not await db.is_user_exist(message.from_user.id):
195
  invite_link = f"https://t.me/{client.me.username}?start=start"
196
+ try:
197
+ await message.reply_text(
198
+ MSG_ERROR_START_BOT.format(invite_link=invite_link),
199
+ disable_web_page_preview=True,
200
+ parse_mode=enums.ParseMode.MARKDOWN,
201
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_START_CHAT, url=invite_link)]]),
202
+ quote=True
203
+ )
204
+ except FloodWait as e:
205
+ await asyncio.sleep(e.value)
206
+ await message.reply_text(
207
+ MSG_ERROR_START_BOT.format(invite_link=invite_link),
208
+ disable_web_page_preview=True,
209
+ parse_mode=enums.ParseMode.MARKDOWN,
210
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_START_CHAT, url=invite_link)]]),
211
+ quote=True
212
+ )
213
  return
214
 
215
  if (message.chat.type in [enums.ChatType.GROUP, enums.ChatType.SUPERGROUP]
 
239
  await reply_user_err(message, MSG_ERROR_INVALID_NUMBER)
240
  return
241
 
242
+ try:
243
+ status_msg = await message.reply_text(MSG_PROCESSING_REQUEST, quote=True)
244
+ except FloodWait as e:
245
+ await asyncio.sleep(e.value)
246
+ status_msg = await message.reply_text(MSG_PROCESSING_REQUEST, quote=True)
247
  shortener_val = handler_kwargs.get('shortener', shortener_val)
248
  if num_files == 1:
249
  await process_single(client, message, message.reply_to_message, status_msg, shortener_val, notification_msg=notification_msg)
 
271
  notification_msg = handler_kwargs.get('notification_msg')
272
 
273
  await log_newusr(client, message.from_user.id, message.from_user.first_name or "")
274
+ try:
275
+ status_msg = await message.reply_text(MSG_PROCESSING_FILE, quote=True)
276
+ except FloodWait as e:
277
+ await asyncio.sleep(e.value)
278
+ status_msg = await message.reply_text(MSG_PROCESSING_FILE, quote=True)
279
  await process_single(client, message, message, status_msg, shortener_val, notification_msg=notification_msg)
280
 
281
  await handle_rate_limited_request(bot, msg, _actual_private_receive_handler, **kwargs)
 
299
 
300
  if is_banned_statically or is_banned_dynamically:
301
  try:
302
+ try:
303
+ await client.leave_chat(message.chat.id)
304
+ except FloodWait as e:
305
+ await asyncio.sleep(e.value)
306
+ await client.leave_chat(message.chat.id)
307
  except Exception as e:
308
  logger.error(f"Error leaving banned channel {message.chat.id}: {e}")
309
  return
 
325
 
326
  if notification_msg:
327
  try:
328
+ try:
329
+ await notification_msg.edit_text(
330
+ MSG_NEW_FILE_REQUEST.format(
331
+ source_info=source_info,
332
+ id_=message.chat.id,
333
+ online_link=links['online_link'],
334
+ stream_link=links['stream_link']
335
+ ),
336
+ disable_web_page_preview=True
337
+ )
338
+ except FloodWait as e:
339
+ await asyncio.sleep(e.value)
340
+ await notification_msg.edit_text(
341
+ MSG_NEW_FILE_REQUEST.format(
342
+ source_info=source_info,
343
+ id_=message.chat.id,
344
+ online_link=links['online_link'],
345
+ stream_link=links['stream_link']
346
+ ),
347
+ disable_web_page_preview=True
348
+ )
349
  except Exception as e:
350
  logger.error(f"Error editing notification message with links: {e}", exc_info=True)
351
  await send_channel_links(stored_msg, links, source_info, message.chat.id)
 
353
  await send_channel_links(stored_msg, links, source_info, message.chat.id)
354
 
355
  try:
356
+ try:
357
+ await message.edit_reply_markup(reply_markup=get_link_buttons(links))
358
+ except FloodWait as e:
359
+ await asyncio.sleep(e.value)
360
+ await message.edit_reply_markup(reply_markup=get_link_buttons(links))
361
  except (MessageNotModified, MessageDeleteForbidden, MessageIdInvalid):
362
  logger.debug(f"Failed to edit reply markup for message {message.id} due to not modified, permissions or invalid ID. Sending new link instead.")
363
  await send_link(message, links)
 
425
  source_info = source_msg.chat.title or "Unknown Channel"
426
  source_id = source_msg.chat.id
427
  if source_info and source_id:
428
+ try:
429
+ await stored_msg.reply_text(
430
+ MSG_NEW_FILE_REQUEST.format(
431
+ source_info=source_info,
432
+ id_=source_id,
433
+ online_link=links['online_link'],
434
+ stream_link=links['stream_link']
435
+ ),
436
+ disable_web_page_preview=True,
437
+ quote=True
438
+ )
439
+ except FloodWait as e:
440
+ await asyncio.sleep(e.value)
441
+ await stored_msg.reply_text(
442
+ MSG_NEW_FILE_REQUEST.format(
443
+ source_info=source_info,
444
+ id_=source_id,
445
+ online_link=links['online_link'],
446
+ stream_link=links['stream_link']
447
+ ),
448
+ disable_web_page_preview=True,
449
+ quote=True
450
+ )
451
  if status_msg:
452
  await safe_delete_message(status_msg)
453
  return links
 
479
  batch_size = min(BATCH_SIZE, count - batch_start)
480
  batch_ids = list(range(start_id + batch_start, start_id + batch_start + batch_size))
481
  try:
482
+ try:
483
+ await status_msg.edit_text(
484
+ MSG_PROCESSING_BATCH.format(
485
+ batch_number=(batch_start // BATCH_SIZE) + 1,
486
+ total_batches=(count + BATCH_SIZE - 1) // BATCH_SIZE,
487
+ file_count=batch_size
488
+ )
489
+ )
490
+ except FloodWait as e:
491
+ await asyncio.sleep(e.value)
492
+ await status_msg.edit_text(
493
+ MSG_PROCESSING_BATCH.format(
494
+ batch_number=(batch_start // BATCH_SIZE) + 1,
495
+ total_batches=(count + BATCH_SIZE - 1) // BATCH_SIZE,
496
+ file_count=batch_size
497
+ )
498
  )
 
499
  except MessageNotModified:
500
  pass
501
  try:
502
+ try:
503
+ messages = await bot.get_messages(msg.chat.id, batch_ids)
504
+ except FloodWait as e:
505
+ await asyncio.sleep(e.value)
506
+ messages = await bot.get_messages(msg.chat.id, batch_ids)
507
  if messages is None:
508
  messages = []
509
  except Exception as e:
 
521
  failed += 1
522
  if (processed + failed) % BATCH_UPDATE_INTERVAL == 0 or (processed + failed) == count:
523
  try:
524
+ try:
525
+ await status_msg.edit_text(
526
+ MSG_PROCESSING_STATUS.format(
527
+ processed=processed,
528
+ total=count,
529
+ failed=failed
530
+ )
531
+ )
532
+ except FloodWait as e:
533
+ await asyncio.sleep(e.value)
534
+ await status_msg.edit_text(
535
+ MSG_PROCESSING_STATUS.format(
536
+ processed=processed,
537
+ total=count,
538
+ failed=failed
539
+ )
540
  )
 
541
  except MessageNotModified:
542
  pass
543
  for i in range(0, len(links_list), LINK_CHUNK_SIZE):
544
  chunk = links_list[i:i+LINK_CHUNK_SIZE]
545
  chunk_text = MSG_BATCH_LINKS_READY.format(count=len(chunk)) + f"\n\n`{chr(10).join(chunk)}`"
546
+ try:
547
+ await msg.reply_text(
548
+ chunk_text,
549
+ quote=True,
550
+ disable_web_page_preview=True,
551
+ parse_mode=enums.ParseMode.MARKDOWN
552
+ )
553
+ except FloodWait as e:
554
+ await asyncio.sleep(e.value)
555
+ await msg.reply_text(
556
+ chunk_text,
557
+ quote=True,
558
+ disable_web_page_preview=True,
559
+ parse_mode=enums.ParseMode.MARKDOWN
560
+ )
561
  if msg.chat.type != enums.ChatType.PRIVATE and msg.from_user:
562
  try:
563
+ try:
564
+ await bot.send_message(
565
+ chat_id=msg.from_user.id,
566
+ text=MSG_DM_BATCH_PREFIX.format(chat_title=msg.chat.title or "the chat") + "\n" + chunk_text,
567
+ disable_web_page_preview=True,
568
+ parse_mode=enums.ParseMode.MARKDOWN
569
+ )
570
+ except FloodWait as e:
571
+ await asyncio.sleep(e.value)
572
+ await bot.send_message(
573
+ chat_id=msg.from_user.id,
574
+ text=MSG_DM_BATCH_PREFIX.format(chat_title=msg.chat.title or "the chat") + "\n" + chunk_text,
575
+ disable_web_page_preview=True,
576
+ parse_mode=enums.ParseMode.MARKDOWN
577
+ )
578
  except Exception as e:
579
  logger.error(f"Error sending DM in batch: {e}", exc_info=True)
580
  await reply_user_err(msg, MSG_ERROR_DM_FAILED)
581
  if i + LINK_CHUNK_SIZE < len(links_list):
582
  await asyncio.sleep(MESSAGE_DELAY)
583
+ try:
584
+ await status_msg.edit_text(
585
+ MSG_PROCESSING_RESULT.format(
586
+ processed=processed,
587
+ total=count,
588
+ failed=failed
589
+ )
590
+ )
591
+ except FloodWait as e:
592
+ await asyncio.sleep(e.value)
593
+ await status_msg.edit_text(
594
+ MSG_PROCESSING_RESULT.format(
595
+ processed=processed,
596
+ total=count,
597
+ failed=failed
598
+ )
599
  )
 
600
  if notification_msg:
601
  await safe_delete_message(notification_msg)
Thunder/utils/bot_utils.py CHANGED
@@ -6,12 +6,12 @@ from urllib.parse import quote
6
 
7
  from pyrogram import Client
8
  from pyrogram.enums import ChatMemberStatus
 
9
  from pyrogram.types import (InlineKeyboardButton, InlineKeyboardMarkup,
10
  Message, User)
11
 
12
  from Thunder.utils.database import db
13
  from Thunder.utils.file_properties import get_fname, get_fsize, get_hash
14
- from Thunder.utils.handler import handle_flood_wait
15
  from Thunder.utils.human_readable import humanbytes
16
  from Thunder.utils.logger import logger
17
  from Thunder.utils.messages import (MSG_BUTTON_GET_HELP, MSG_DC_UNKNOWN,
@@ -24,24 +24,43 @@ from Thunder.vars import Var
24
  async def notify_ch(cli: Client, txt: str):
25
  if not (hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0):
26
  return
27
- await handle_flood_wait(cli.send_message, chat_id=Var.BIN_CHANNEL, text=txt)
 
 
 
 
28
 
29
 
30
  async def notify_own(cli: Client, txt: str):
31
  o_ids = Var.OWNER_ID if isinstance(Var.OWNER_ID, (list, tuple, set)) else [Var.OWNER_ID]
32
- tasks = [handle_flood_wait(cli.send_message, chat_id=oid, text=txt) for oid in o_ids]
 
 
 
 
 
 
 
 
33
  if hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0:
34
- tasks.append(handle_flood_wait(cli.send_message, chat_id=Var.BIN_CHANNEL, text=txt))
35
  await asyncio.gather(*tasks, return_exceptions=True)
36
 
37
 
38
  async def reply_user_err(msg: Message, err_txt: str):
39
- await handle_flood_wait(
40
- msg.reply_text,
41
- text=err_txt,
42
- reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_GET_HELP, callback_data="help_command")]]),
43
- disable_web_page_preview=True
44
- )
 
 
 
 
 
 
 
45
 
46
 
47
  async def log_newusr(cli: Client, uid: int, fname: str):
@@ -50,7 +69,11 @@ async def log_newusr(cli: Client, uid: int, fname: str):
50
  return
51
  await db.add_user(uid)
52
  if hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0:
53
- await handle_flood_wait(cli.send_message, chat_id=Var.BIN_CHANNEL, text=MSG_NEW_USER.format(first_name=fname, user_id=uid))
 
 
 
 
54
  except Exception as e:
55
  logger.error(f"Database error in log_newusr for user {uid}: {e}")
56
 
@@ -91,20 +114,40 @@ async def gen_dc_txt(usr: User) -> str:
91
  async def get_user(cli: Client, qry: Any) -> Optional[User]:
92
  if isinstance(qry, str):
93
  if qry.startswith('@'):
94
- return await handle_flood_wait(cli.get_users, qry)
 
 
 
 
95
  elif qry.isdigit():
96
- return await handle_flood_wait(cli.get_users, int(qry))
 
 
 
 
97
  elif isinstance(qry, int):
98
- return await handle_flood_wait(cli.get_users, qry)
 
 
 
 
99
  return None
100
 
101
 
102
  async def is_admin(cli: Client, chat_id_val: int) -> bool:
103
- member = await handle_flood_wait(cli.get_chat_member, chat_id_val, cli.me.id)
 
 
 
 
104
  if member is None:
105
  return False
106
  return member.status in [ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]
107
 
108
 
109
  async def reply(msg: Message, **kwargs):
110
- return await handle_flood_wait(msg.reply_text, **kwargs, quote=True, disable_web_page_preview=True)
 
 
 
 
 
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
  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.human_readable import humanbytes
16
  from Thunder.utils.logger import logger
17
  from Thunder.utils.messages import (MSG_BUTTON_GET_HELP, MSG_DC_UNKNOWN,
 
24
  async def notify_ch(cli: Client, txt: str):
25
  if not (hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0):
26
  return
27
+ try:
28
+ await cli.send_message(chat_id=Var.BIN_CHANNEL, text=txt)
29
+ except FloodWait as e:
30
+ await asyncio.sleep(e.value)
31
+ await cli.send_message(chat_id=Var.BIN_CHANNEL, text=txt)
32
 
33
 
34
  async def notify_own(cli: Client, txt: str):
35
  o_ids = Var.OWNER_ID if isinstance(Var.OWNER_ID, (list, tuple, set)) else [Var.OWNER_ID]
36
+
37
+ async def send_with_flood_wait(chat_id: int):
38
+ try:
39
+ await cli.send_message(chat_id=chat_id, text=txt)
40
+ except FloodWait as e:
41
+ await asyncio.sleep(e.value)
42
+ await cli.send_message(chat_id=chat_id, text=txt)
43
+
44
+ tasks = [send_with_flood_wait(oid) for oid in o_ids]
45
  if hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0:
46
+ tasks.append(send_with_flood_wait(Var.BIN_CHANNEL))
47
  await asyncio.gather(*tasks, return_exceptions=True)
48
 
49
 
50
  async def reply_user_err(msg: Message, err_txt: str):
51
+ try:
52
+ await msg.reply_text(
53
+ text=err_txt,
54
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_GET_HELP, callback_data="help_command")]]),
55
+ disable_web_page_preview=True
56
+ )
57
+ except FloodWait as e:
58
+ await asyncio.sleep(e.value)
59
+ await msg.reply_text(
60
+ text=err_txt,
61
+ reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton(MSG_BUTTON_GET_HELP, callback_data="help_command")]]),
62
+ disable_web_page_preview=True
63
+ )
64
 
65
 
66
  async def log_newusr(cli: Client, uid: int, fname: str):
 
69
  return
70
  await db.add_user(uid)
71
  if hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0:
72
+ try:
73
+ await cli.send_message(chat_id=Var.BIN_CHANNEL, text=MSG_NEW_USER.format(first_name=fname, user_id=uid))
74
+ except FloodWait as e:
75
+ await asyncio.sleep(e.value)
76
+ await cli.send_message(chat_id=Var.BIN_CHANNEL, text=MSG_NEW_USER.format(first_name=fname, user_id=uid))
77
  except Exception as e:
78
  logger.error(f"Database error in log_newusr for user {uid}: {e}")
79
 
 
114
  async def get_user(cli: Client, qry: Any) -> Optional[User]:
115
  if isinstance(qry, str):
116
  if qry.startswith('@'):
117
+ try:
118
+ return await cli.get_users(qry)
119
+ except FloodWait as e:
120
+ await asyncio.sleep(e.value)
121
+ return await cli.get_users(qry)
122
  elif qry.isdigit():
123
+ try:
124
+ return await cli.get_users(int(qry))
125
+ except FloodWait as e:
126
+ await asyncio.sleep(e.value)
127
+ return await cli.get_users(int(qry))
128
  elif isinstance(qry, int):
129
+ try:
130
+ return await cli.get_users(qry)
131
+ except FloodWait as e:
132
+ await asyncio.sleep(e.value)
133
+ return await cli.get_users(qry)
134
  return None
135
 
136
 
137
  async def is_admin(cli: Client, chat_id_val: int) -> bool:
138
+ try:
139
+ member = await cli.get_chat_member(chat_id_val, cli.me.id)
140
+ except FloodWait as e:
141
+ await asyncio.sleep(e.value)
142
+ member = await cli.get_chat_member(chat_id_val, cli.me.id)
143
  if member is None:
144
  return False
145
  return member.status in [ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]
146
 
147
 
148
  async def reply(msg: Message, **kwargs):
149
+ try:
150
+ return await msg.reply_text(**kwargs, quote=True, disable_web_page_preview=True)
151
+ except FloodWait as e:
152
+ await asyncio.sleep(e.value)
153
+ return await msg.reply_text(**kwargs, quote=True, disable_web_page_preview=True)
Thunder/utils/broadcast.py CHANGED
@@ -12,7 +12,6 @@ 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
  MSG_INVALID_BROADCAST_CMD,
@@ -27,20 +26,32 @@ broadcast_ids = {}
27
 
28
  async def broadcast_message(client: Client, message: Message):
29
  if not message.reply_to_message:
30
- await handle_flood_wait(message.reply_text, MSG_INVALID_BROADCAST_CMD)
 
 
 
 
31
  return
32
 
33
  broadcast_id = os.urandom(3).hex()
34
  stats = {"total": 0, "success": 0, "failed": 0, "deleted": 0, "cancelled": False}
35
  broadcast_ids[broadcast_id] = stats
36
 
37
- status_msg = await handle_flood_wait(
38
- message.reply_text,
39
- MSG_BROADCAST_START,
40
- reply_markup=InlineKeyboardMarkup([[
41
- InlineKeyboardButton(MSG_BUTTON_CANCEL_BROADCAST, callback_data=f"cancel_{broadcast_id}")
42
- ]])
43
- )
 
 
 
 
 
 
 
 
44
 
45
  start_time = time.time()
46
  stats["total"] = await db.total_users_count()
@@ -87,26 +98,52 @@ async def broadcast_message(client: Client, message: Message):
87
 
88
  except FloodWait as e:
89
  await asyncio.sleep(e.value)
90
- if await handle_flood_wait(message.reply_to_message.copy, user['id']):
91
- stats["success"] += 1
92
- else:
93
- stats["failed"] += 1
 
 
 
 
 
 
 
 
 
94
  except Exception as e:
95
  logger.error(f"Error copying message to user {user['id']}: {e}", exc_info=True)
96
  stats["failed"] += 1
97
 
98
- await handle_flood_wait(status_msg.delete)
99
- await handle_flood_wait(
100
- message.reply_text,
101
- MSG_BROADCAST_COMPLETE.format(
102
- elapsed_time=get_readable_time(int(time.time() - start_time)),
103
- total_users=stats["total"],
104
- successes=stats["success"],
105
- failures=stats["failed"],
106
- deleted_accounts=stats["deleted"]
107
- ),
108
- parse_mode=ParseMode.MARKDOWN
109
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  del broadcast_ids[broadcast_id]
112
 
 
12
  Message)
13
 
14
  from Thunder.utils.database import db
 
15
  from Thunder.utils.logger import logger
16
  from Thunder.utils.messages import (
17
  MSG_INVALID_BROADCAST_CMD,
 
26
 
27
  async def broadcast_message(client: Client, message: Message):
28
  if not message.reply_to_message:
29
+ try:
30
+ await message.reply_text(MSG_INVALID_BROADCAST_CMD)
31
+ except FloodWait as e:
32
+ await asyncio.sleep(e.value)
33
+ await message.reply_text(MSG_INVALID_BROADCAST_CMD)
34
  return
35
 
36
  broadcast_id = os.urandom(3).hex()
37
  stats = {"total": 0, "success": 0, "failed": 0, "deleted": 0, "cancelled": False}
38
  broadcast_ids[broadcast_id] = stats
39
 
40
+ try:
41
+ status_msg = await message.reply_text(
42
+ MSG_BROADCAST_START,
43
+ reply_markup=InlineKeyboardMarkup([[
44
+ InlineKeyboardButton(MSG_BUTTON_CANCEL_BROADCAST, callback_data=f"cancel_{broadcast_id}")
45
+ ]])
46
+ )
47
+ except FloodWait as e:
48
+ await asyncio.sleep(e.value)
49
+ status_msg = await message.reply_text(
50
+ MSG_BROADCAST_START,
51
+ reply_markup=InlineKeyboardMarkup([[
52
+ InlineKeyboardButton(MSG_BUTTON_CANCEL_BROADCAST, callback_data=f"cancel_{broadcast_id}")
53
+ ]])
54
+ )
55
 
56
  start_time = time.time()
57
  stats["total"] = await db.total_users_count()
 
98
 
99
  except FloodWait as e:
100
  await asyncio.sleep(e.value)
101
+ try:
102
+ result = await message.reply_to_message.copy(user['id'])
103
+ if result:
104
+ stats["success"] += 1
105
+ else:
106
+ stats["failed"] += 1
107
+ except FloodWait as e2:
108
+ await asyncio.sleep(e2.value)
109
+ result = await message.reply_to_message.copy(user['id'])
110
+ if result:
111
+ stats["success"] += 1
112
+ else:
113
+ stats["failed"] += 1
114
  except Exception as e:
115
  logger.error(f"Error copying message to user {user['id']}: {e}", exc_info=True)
116
  stats["failed"] += 1
117
 
118
+ try:
119
+ await status_msg.delete()
120
+ except FloodWait as e:
121
+ await asyncio.sleep(e.value)
122
+ await status_msg.delete()
123
+
124
+ try:
125
+ await message.reply_text(
126
+ MSG_BROADCAST_COMPLETE.format(
127
+ elapsed_time=get_readable_time(int(time.time() - start_time)),
128
+ total_users=stats["total"],
129
+ successes=stats["success"],
130
+ failures=stats["failed"],
131
+ deleted_accounts=stats["deleted"]
132
+ ),
133
+ parse_mode=ParseMode.MARKDOWN
134
+ )
135
+ except FloodWait as e:
136
+ await asyncio.sleep(e.value)
137
+ await message.reply_text(
138
+ MSG_BROADCAST_COMPLETE.format(
139
+ elapsed_time=get_readable_time(int(time.time() - start_time)),
140
+ total_users=stats["total"],
141
+ successes=stats["success"],
142
+ failures=stats["failed"],
143
+ deleted_accounts=stats["deleted"]
144
+ ),
145
+ parse_mode=ParseMode.MARKDOWN
146
+ )
147
 
148
  del broadcast_ids[broadcast_id]
149
 
Thunder/utils/database.py CHANGED
@@ -2,20 +2,21 @@
2
 
3
  import datetime
4
  from typing import Optional, Dict, Any
5
- from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection
 
6
  from Thunder.vars import Var
7
  from Thunder.utils.logger import logger
8
 
9
  class Database:
10
  def __init__(self, uri: str, database_name: str, *args, **kwargs):
11
- self._client = AsyncIOMotorClient(uri, *args, **kwargs)
12
  self.db = self._client[database_name]
13
- self.col: AsyncIOMotorCollection = self.db.users
14
- self.banned_users_col: AsyncIOMotorCollection = self.db.banned_users
15
- self.banned_channels_col: AsyncIOMotorCollection = self.db.banned_channels
16
- self.token_col: AsyncIOMotorCollection = self.db.tokens
17
- self.authorized_users_col: AsyncIOMotorCollection = self.db.authorized_users
18
- self.restart_message_col: AsyncIOMotorCollection = self.db.restart_message
19
 
20
  async def ensure_indexes(self):
21
  try:
@@ -208,6 +209,6 @@ class Database:
208
 
209
  async def close(self):
210
  if self._client:
211
- self._client.close()
212
 
213
  db = Database(Var.DATABASE_URL, Var.NAME)
 
2
 
3
  import datetime
4
  from typing import Optional, Dict, Any
5
+ from pymongo import AsyncMongoClient
6
+ from pymongo.asynchronous.collection import AsyncCollection
7
  from Thunder.vars import Var
8
  from Thunder.utils.logger import logger
9
 
10
  class Database:
11
  def __init__(self, uri: str, database_name: str, *args, **kwargs):
12
+ self._client = AsyncMongoClient(uri, *args, **kwargs)
13
  self.db = self._client[database_name]
14
+ self.col: AsyncCollection = self.db.users
15
+ self.banned_users_col: AsyncCollection = self.db.banned_users
16
+ self.banned_channels_col: AsyncCollection = self.db.banned_channels
17
+ self.token_col: AsyncCollection = self.db.tokens
18
+ self.authorized_users_col: AsyncCollection = self.db.authorized_users
19
+ self.restart_message_col: AsyncCollection = self.db.restart_message
20
 
21
  async def ensure_indexes(self):
22
  try:
 
209
 
210
  async def close(self):
211
  if self._client:
212
+ await self._client.close()
213
 
214
  db = Database(Var.DATABASE_URL, Var.NAME)
Thunder/utils/decorators.py CHANGED
@@ -1,9 +1,10 @@
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)
@@ -28,14 +29,23 @@ async def check_banned(client, message: Message):
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
36
- ),
37
- quote=True
38
- )
 
 
 
 
 
 
 
 
 
39
  logger.debug(f"Blocked banned user {user_id}.")
40
  return False
41
  return True
@@ -60,18 +70,34 @@ 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 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
@@ -83,20 +109,33 @@ async def require_token(client, message: Message):
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)]
91
- ]),
92
- quote=True
93
- )
 
 
 
 
 
 
 
 
 
94
  logger.debug(f"Sent temporary token activation link to user {user_id}.")
95
  return False
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
 
1
  # Thunder/utils/decorators.py
2
 
3
+ import asyncio
4
+ from pyrogram.errors import FloodWait
5
  from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
6
 
7
  from Thunder.utils.database import db
 
8
  from Thunder.utils.logger import logger
9
  from Thunder.utils.messages import (MSG_DECORATOR_BANNED,
10
  MSG_ERROR_UNAUTHORIZED, MSG_TOKEN_INVALID)
 
29
  if banned_at and hasattr(banned_at, 'strftime')
30
  else str(banned_at) if banned_at else 'N/A'
31
  )
32
+ try:
33
+ await message.reply_text(
34
+ MSG_DECORATOR_BANNED.format(
35
+ reason=ban_details.get('reason', 'Not specified'),
36
+ ban_time=ban_time
37
+ ),
38
+ quote=True
39
+ )
40
+ except FloodWait as e:
41
+ await asyncio.sleep(e.value)
42
+ await message.reply_text(
43
+ MSG_DECORATOR_BANNED.format(
44
+ reason=ban_details.get('reason', 'Not specified'),
45
+ ban_time=ban_time
46
+ ),
47
+ quote=True
48
+ )
49
  logger.debug(f"Blocked banned user {user_id}.")
50
  return False
51
  return True
 
70
  temp_token_string = await generate(user_id)
71
  except Exception as e:
72
  logger.error(f"Failed to generate temporary token for user {user_id} in require_token: {e}", exc_info=True)
73
+ try:
74
+ await message.reply_text("Sorry, could not generate an access token link. Please try again later.", quote=True)
75
+ except FloodWait as e:
76
+ await asyncio.sleep(e.value)
77
+ await message.reply_text("Sorry, could not generate an access token link. Please try again later.", quote=True)
78
  return False
79
 
80
  if not temp_token_string:
81
  logger.error(f"Temporary token generation returned empty for user {user_id} in require_token.", exc_info=True)
82
+ try:
83
+ await message.reply_text("Sorry, could not generate an access token link. Please try again later.", quote=True)
84
+ except FloodWait as e:
85
+ await asyncio.sleep(e.value)
86
+ await message.reply_text("Sorry, could not generate an access token link. Please try again later.", quote=True)
87
  return False
88
 
89
+ try:
90
+ me = await client.get_me()
91
+ except FloodWait as e:
92
+ await asyncio.sleep(e.value)
93
+ me = await client.get_me()
94
  if not me:
95
  logger.error(f"Failed to get bot info for user {user_id} in require_token.", exc_info=True)
96
+ try:
97
+ await message.reply_text("Sorry, an unexpected error occurred. Please try again later.", quote=True)
98
+ except FloodWait as e:
99
+ await asyncio.sleep(e.value)
100
+ await message.reply_text("Sorry, an unexpected error occurred. Please try again later.", quote=True)
101
  return False
102
  deep_link = f"https://t.me/{me.username}?start={temp_token_string}"
103
  short_url = deep_link
 
109
  except Exception as e:
110
  logger.warning(f"Failed to shorten token link for user {user_id}: {e}. Using full link.", exc_info=True)
111
 
112
+ try:
113
+ await message.reply_text(
114
+ MSG_TOKEN_INVALID,
115
+ reply_markup=InlineKeyboardMarkup([
116
+ [InlineKeyboardButton("Activate Access", url=short_url)]
117
+ ]),
118
+ quote=True
119
+ )
120
+ except FloodWait as e:
121
+ await asyncio.sleep(e.value)
122
+ await message.reply_text(
123
+ MSG_TOKEN_INVALID,
124
+ reply_markup=InlineKeyboardMarkup([
125
+ [InlineKeyboardButton("Activate Access", url=short_url)]
126
+ ]),
127
+ quote=True
128
+ )
129
  logger.debug(f"Sent temporary token activation link to user {user_id}.")
130
  return False
131
  except Exception as e:
132
  logger.error(f"Error in require_token: {e}", exc_info=True)
133
  try:
134
+ try:
135
+ await message.reply_text("An error occurred while checking your authorization. Please try again.", quote=True)
136
+ except FloodWait as e:
137
+ await asyncio.sleep(e.value)
138
+ await message.reply_text("An error occurred while checking your authorization. Please try again.", quote=True)
139
  except Exception as inner_e:
140
  logger.error(f"Failed to send error message to user in require_token: {inner_e}", exc_info=True)
141
  return False
Thunder/utils/file_properties.py CHANGED
@@ -1,14 +1,15 @@
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
 
@@ -78,7 +79,11 @@ def get_fname(msg: Message) -> str:
78
 
79
  async def get_fids(client: Client, chat_id: int, message_id: int) -> FileId:
80
  try:
81
- msg = await handle_flood_wait(client.get_messages, chat_id, message_id)
 
 
 
 
82
 
83
  if not msg or getattr(msg, 'empty', False):
84
  raise FileNotFound("Message not found")
 
1
  # Thunder/utils/file_properties.py
2
 
3
+ import asyncio
4
  from datetime import datetime as dt
5
  from typing import Any, Optional
6
 
7
  from pyrogram.client import Client
8
+ from pyrogram.errors import FloodWait
9
  from pyrogram.file_id import FileId
10
  from pyrogram.types import Message
11
 
12
  from Thunder.server.exceptions import FileNotFound
 
13
  from Thunder.utils.logger import logger
14
 
15
 
 
79
 
80
  async def get_fids(client: Client, chat_id: int, message_id: int) -> FileId:
81
  try:
82
+ try:
83
+ msg = await client.get_messages(chat_id, message_id)
84
+ except FloodWait as e:
85
+ await asyncio.sleep(e.value)
86
+ msg = await client.get_messages(chat_id, message_id)
87
 
88
  if not msg or getattr(msg, 'empty', False):
89
  raise FileNotFound("Message not found")
Thunder/utils/force_channel.py CHANGED
@@ -6,7 +6,6 @@ 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
@@ -24,7 +23,11 @@ async def get_force_info(bot: Client):
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"
@@ -54,17 +57,33 @@ async def force_channel_check(client: Client, message: Message):
54
  except UserNotParticipant:
55
  link, title = await get_force_info(client)
56
  if link and title:
57
- await handle_flood_wait(
58
- message.reply_text,
59
- MSG_COMMUNITY_CHANNEL.format(channel_title=title),
60
- reply_markup=InlineKeyboardMarkup([[
61
- InlineKeyboardButton("Join", url=link)
62
- ]])
63
- )
 
 
 
 
 
 
 
 
64
  else:
65
- await handle_flood_wait(message.reply_text, "You must join the channel to use this bot.")
 
 
 
 
66
  return False
67
  except Exception as e:
68
  logger.error(f"Error checking force channel: {e}", exc_info=True)
69
- await handle_flood_wait(message.reply_text, "An unexpected error occurred while checking channel membership. Please try again.")
 
 
 
 
70
  return False
 
6
  from pyrogram.errors import FloodWait, UserNotParticipant
7
  from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message
8
 
 
9
  from Thunder.utils.logger import logger
10
  from Thunder.utils.messages import MSG_COMMUNITY_CHANNEL
11
  from Thunder.vars import Var
 
23
  return _force_link, _force_title
24
 
25
  try:
26
+ try:
27
+ chat = await bot.get_chat(Var.FORCE_CHANNEL_ID)
28
+ except FloodWait as e:
29
+ await asyncio.sleep(e.value)
30
+ chat = await bot.get_chat(Var.FORCE_CHANNEL_ID)
31
  if chat:
32
  _force_link = chat.invite_link or (f"https://t.me/{chat.username}" if chat.username else None)
33
  _force_title = chat.title or "Channel"
 
57
  except UserNotParticipant:
58
  link, title = await get_force_info(client)
59
  if link and title:
60
+ try:
61
+ await message.reply_text(
62
+ MSG_COMMUNITY_CHANNEL.format(channel_title=title),
63
+ reply_markup=InlineKeyboardMarkup([[
64
+ InlineKeyboardButton("Join", url=link)
65
+ ]])
66
+ )
67
+ except FloodWait as e:
68
+ await asyncio.sleep(e.value)
69
+ await message.reply_text(
70
+ MSG_COMMUNITY_CHANNEL.format(channel_title=title),
71
+ reply_markup=InlineKeyboardMarkup([[
72
+ InlineKeyboardButton("Join", url=link)
73
+ ]])
74
+ )
75
  else:
76
+ try:
77
+ await message.reply_text("You must join the channel to use this bot.")
78
+ except FloodWait as e:
79
+ await asyncio.sleep(e.value)
80
+ await message.reply_text("You must join the channel to use this bot.")
81
  return False
82
  except Exception as e:
83
  logger.error(f"Error checking force channel: {e}", exc_info=True)
84
+ try:
85
+ await message.reply_text("An unexpected error occurred while checking channel membership. Please try again.")
86
+ except FloodWait as e:
87
+ await asyncio.sleep(e.value)
88
+ await message.reply_text("An unexpected error occurred while checking channel membership. Please try again.")
89
  return False
Thunder/utils/handler.py DELETED
@@ -1,31 +0,0 @@
1
- # Thunder/utils/handler.py
2
-
3
- import asyncio
4
- from typing import Callable
5
-
6
- from pyrogram.errors import FloodWait, MessageNotModified
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', 3)
13
- delay = kwargs.pop('delay', 3)
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 MessageNotModified:
23
- logger.debug(f"MessageNotModified in '{func.__name__}' - not retrying as content hasn't changed.")
24
- raise
25
- except Exception:
26
- logger.error(f"An exception occurred in '{func.__name__}' on retry {i + 1}/{retries}", exc_info=True)
27
- if i < retries - 1:
28
- await asyncio.sleep(delay)
29
- else:
30
- raise
31
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Thunder/utils/rate_limiter.py CHANGED
@@ -15,7 +15,6 @@ from Thunder.utils.messages import (
15
  MSG_RATE_LIMIT_QUEUE_REGULAR,
16
  MSG_RATE_LIMIT_QUEUE_FULL
17
  )
18
- from Thunder.utils.handler import handle_flood_wait
19
  from Thunder.vars import Var
20
 
21
 
@@ -228,20 +227,20 @@ class RateLimiter:
228
  self.user_queue_counts.pop(user_id, None)
229
 
230
  except asyncio.CancelledError:
231
- logger.info("Request executor cancelled, shutting down.")
232
  break
233
  except Exception as e:
234
  logger.critical(f"Critical error in request executor: {e}", exc_info=True)
235
  await asyncio.sleep(5)
236
 
237
  async def shutdown(self):
238
- logger.info("Shutting down rate limiter and clearing queues...")
239
  async with self.request_lock:
240
  self.request_queue.clear()
241
  self.priority_queue.clear()
242
  self.user_queue_counts.clear()
243
  self.request_event.clear()
244
- logger.info("Rate limiter queues cleared.")
245
 
246
  def get_queue_status(self) -> dict:
247
  return {
@@ -390,12 +389,19 @@ async def _send_notification(bot: Client, message: Message, template: str, file_
390
 
391
  text = template.format(wait_estimate=wait_estimate, s="s" if wait_estimate > 1 else "", **format_kwargs)
392
 
393
- return await handle_flood_wait(
394
- bot.send_message,
395
- chat_id=message.chat.id,
396
- text=text,
397
- reply_to_message_id=message.id
398
- )
 
 
 
 
 
 
 
399
  else:
400
  logger.debug("Skipping notification for channel message (no from_user)")
401
  return None
 
15
  MSG_RATE_LIMIT_QUEUE_REGULAR,
16
  MSG_RATE_LIMIT_QUEUE_FULL
17
  )
 
18
  from Thunder.vars import Var
19
 
20
 
 
227
  self.user_queue_counts.pop(user_id, None)
228
 
229
  except asyncio.CancelledError:
230
+ logger.debug("Request executor cancelled, shutting down.")
231
  break
232
  except Exception as e:
233
  logger.critical(f"Critical error in request executor: {e}", exc_info=True)
234
  await asyncio.sleep(5)
235
 
236
  async def shutdown(self):
237
+ logger.debug("Shutting down rate limiter and clearing queues...")
238
  async with self.request_lock:
239
  self.request_queue.clear()
240
  self.priority_queue.clear()
241
  self.user_queue_counts.clear()
242
  self.request_event.clear()
243
+ logger.debug("Rate limiter queues cleared.")
244
 
245
  def get_queue_status(self) -> dict:
246
  return {
 
389
 
390
  text = template.format(wait_estimate=wait_estimate, s="s" if wait_estimate > 1 else "", **format_kwargs)
391
 
392
+ try:
393
+ return await bot.send_message(
394
+ chat_id=message.chat.id,
395
+ text=text,
396
+ reply_to_message_id=message.id
397
+ )
398
+ except FloodWait as e:
399
+ await asyncio.sleep(e.value)
400
+ return await bot.send_message(
401
+ chat_id=message.chat.id,
402
+ text=text,
403
+ reply_to_message_id=message.id
404
+ )
405
  else:
406
  logger.debug("Skipping notification for channel message (no from_user)")
407
  return None
Thunder/utils/render_template.py CHANGED
@@ -1,14 +1,15 @@
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,7 +23,12 @@ template_env = Environment(
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
 
 
1
  # Thunder/utils/render_template.py
2
 
3
+ import asyncio
4
  import html as html_module
5
  import urllib.parse
6
 
7
  from jinja2 import Environment, FileSystemLoader
8
+ from pyrogram.errors import FloodWait
9
 
10
  from Thunder.bot import StreamBot
11
  from Thunder.server.exceptions import InvalidHash
12
  from Thunder.utils.file_properties import get_fname, get_uniqid
 
13
  from Thunder.utils.logger import logger
14
  from Thunder.vars import Var
15
 
 
23
 
24
  async def render_page(id: int, secure_hash: str, requested_action: str | None = None) -> str:
25
  try:
26
+ try:
27
+ message = await StreamBot.get_messages(chat_id=int(Var.BIN_CHANNEL), message_ids=id)
28
+ except FloodWait as e:
29
+ await asyncio.sleep(e.value)
30
+ message = await StreamBot.get_messages(chat_id=int(Var.BIN_CHANNEL), message_ids=id)
31
+
32
  if not message:
33
  raise InvalidHash("Message not found")
34
 
Thunder/utils/speedtest.py CHANGED
@@ -31,7 +31,7 @@ def _perform_speedtest() -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
31
  results['download_bps'] = st.results.download / 8
32
  results['upload_bps'] = st.results.upload / 8
33
 
34
- logger.info(f"Download: {download_mbps:.2f} Mbps | Upload: {upload_mbps:.2f} Mbps")
35
 
36
  try:
37
  return results, st.results.share()
 
31
  results['download_bps'] = st.results.download / 8
32
  results['upload_bps'] = st.results.upload / 8
33
 
34
+ logger.debug(f"Download: {download_mbps:.2f} Mbps | Upload: {upload_mbps:.2f} Mbps")
35
 
36
  try:
37
  return results, st.results.share()
Thunder/vars.py CHANGED
@@ -53,10 +53,8 @@ class Var:
53
  if not OWNER_ID:
54
  logger.warning("WARNING: OWNER_ID is not set. No user will be granted owner access.")
55
 
56
- OWNER_USERNAME: str = os.getenv("OWNER_USERNAME", "")
57
-
58
  FQDN: str = os.getenv("FQDN", "") or BIND_ADDRESS
59
- HAS_SSL: bool = str_to_bool(os.getenv("HAS_SSL", "False"))
60
  PROTOCOL: str = "https" if HAS_SSL else "http"
61
  PORT_SEGMENT: str = "" if NO_PORT else f":{PORT}"
62
  URL: str = f"{PROTOCOL}://{FQDN}{PORT_SEGMENT}/"
 
53
  if not OWNER_ID:
54
  logger.warning("WARNING: OWNER_ID is not set. No user will be granted owner access.")
55
 
 
 
56
  FQDN: str = os.getenv("FQDN", "") or BIND_ADDRESS
57
+ HAS_SSL: bool = str_to_bool(os.getenv("HAS_SSL", "True"))
58
  PROTOCOL: str = "https" if HAS_SSL else "http"
59
  PORT_SEGMENT: str = "" if NO_PORT else f":{PORT}"
60
  URL: str = f"{PROTOCOL}://{FQDN}{PORT_SEGMENT}/"
config_sample.env CHANGED
@@ -17,15 +17,14 @@ BOT_TOKEN="" # Example: "123456789:ABCdef..."
17
  BIN_CHANNEL=0 # Example: -1001234567890
18
 
19
  # Owner information (get ID from @userinfobot)
20
- OWNER_ID="" # Your Telegram user ID(s) as a space-separated string. Example: "123456789 098765432"
21
- OWNER_USERNAME="" # Example: "@YourUsername"
22
 
23
  # Database connection string
24
  DATABASE_URL="" # Example: "mongodb+srv://user:pass@host/db"
25
 
26
  # Deployment configuration
27
  FQDN="" # Your domain name
28
- HAS_SSL="False" # Set to "True" if using HTTPS
29
  PORT=8080 # Web server port
30
  NO_PORT="True" # Hide port in URLs ("True" or "False")
31
 
@@ -123,3 +122,4 @@ WORKERS=8 # Number of worker processes
123
  # Web server configuration
124
  BIND_ADDRESS="0.0.0.0" # Listen on all network interfaces
125
  PING_INTERVAL=840 # Ping interval in seconds
 
 
17
  BIN_CHANNEL=0 # Example: -1001234567890
18
 
19
  # Owner information (get ID from @userinfobot)
20
+ OWNER_ID=0 # Your Telegram user ID. Example: 123456789
 
21
 
22
  # Database connection string
23
  DATABASE_URL="" # Example: "mongodb+srv://user:pass@host/db"
24
 
25
  # Deployment configuration
26
  FQDN="" # Your domain name
27
+ HAS_SSL="True" # Set to "True" if using HTTPS
28
  PORT=8080 # Web server port
29
  NO_PORT="True" # Hide port in URLs ("True" or "False")
30
 
 
122
  # Web server configuration
123
  BIND_ADDRESS="0.0.0.0" # Listen on all network interfaces
124
  PING_INTERVAL=840 # Ping interval in seconds
125
+
requirements.txt CHANGED
@@ -2,7 +2,7 @@ aiohttp
2
  cloudscraper
3
  Jinja2
4
  pyrofork
5
- motor
6
  psutil
7
  python-dotenv
8
  speedtest-cli
 
2
  cloudscraper
3
  Jinja2
4
  pyrofork
5
+ pymongo
6
  psutil
7
  python-dotenv
8
  speedtest-cli