Spaces:
Running
Running
Fyaz Mohammed commited on
1.9.9
Browse files- 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 +25 -0
- README.md +139 -87
- Thunder/__init__.py +1 -1
- Thunder/__main__.py +31 -10
- Thunder/bot/clients.py +11 -3
- Thunder/bot/plugins/admin.py +91 -21
- Thunder/bot/plugins/callbacks.py +112 -39
- Thunder/bot/plugins/common.py +79 -22
- Thunder/bot/plugins/stream.py +252 -107
- Thunder/utils/bot_utils.py +59 -16
- Thunder/utils/broadcast.py +62 -25
- Thunder/utils/database.py +10 -9
- Thunder/utils/decorators.py +61 -22
- Thunder/utils/file_properties.py +7 -2
- Thunder/utils/force_channel.py +30 -11
- Thunder/utils/handler.py +0 -31
- Thunder/utils/rate_limiter.py +16 -10
- Thunder/utils/render_template.py +8 -2
- Thunder/utils/speedtest.py +1 -1
- Thunder/vars.py +1 -3
- config_sample.env +3 -3
- requirements.txt +1 -1
.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 |
-
- [
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
| 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 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
|
| 329 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
-
|
|
|
|
|
|
|
| 332 |
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
|
| 338 |
<details>
|
| 339 |
-
<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 |
-
**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
-
|
| 439 |
-
- Ensure your server's firewall is not blocking the bot's port.
|
| 440 |
-
- View bot logs for any error messages.
|
| 441 |
|
| 442 |
-
**
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
|
| 444 |
-
|
| 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
|
| 449 |
-
A:
|
|
|
|
| 450 |
|
| 451 |
-
**Q:
|
| 452 |
-
A:
|
| 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 |
-
- [
|
| 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 |
+
[](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.
|
|
|
|
| 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 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 115 |
-
StreamBot.edit_message_text
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 160 |
-
message.reply_document
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
|
| 336 |
if len(output) > 4096:
|
| 337 |
file = BytesIO(output.encode())
|
| 338 |
file.name = "shell_output.txt"
|
| 339 |
-
|
| 340 |
-
message.reply_document
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
else:
|
| 344 |
await reply(message, text=output, parse_mode=ParseMode.HTML)
|
| 345 |
|
| 346 |
except Exception as e:
|
| 347 |
try:
|
| 348 |
-
|
| 349 |
-
status_msg.edit_text
|
| 350 |
-
|
| 351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
else:
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 47 |
-
callback_query.message.edit_text
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
except MessageNotModified:
|
| 53 |
pass
|
| 54 |
except Exception as e:
|
| 55 |
logger.error(f"Error in help callback: {e}", exc_info=True)
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 70 |
-
callback_query.message.edit_text
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
except MessageNotModified:
|
| 76 |
pass
|
| 77 |
except Exception as e:
|
| 78 |
logger.error(f"Error in about callback: {e}", exc_info=True)
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 94 |
-
callback_query.message.edit_text
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
except Exception as e:
|
| 100 |
logger.error(f"Error in restart broadcast callback: {e}", exc_info=True)
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 132 |
-
callback_query.message.edit_text
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
else:
|
| 136 |
-
|
| 137 |
-
callback_query.answer
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
except Exception as e:
|
| 142 |
logger.error(f"Error in cancel broadcast callback: {e}", exc_info=True)
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
@StreamBot.on_callback_query()
|
| 146 |
async def fallback_callback(client: Client, callback_query: CallbackQuery):
|
| 147 |
try:
|
| 148 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
if token.get("activated"):
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
else:
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 69 |
-
target_msg.reply_text
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
async def safe_edit_message(message: Message, text: str, **kwargs):
|
| 82 |
try:
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 111 |
-
bot.send_message
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 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 |
-
|
| 124 |
-
msg.reply_text
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 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 |
-
|
| 147 |
-
message.reply_text
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 258 |
-
notification_msg.edit_text
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 343 |
-
stored_msg.reply_text
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 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 |
-
|
| 385 |
-
status_msg.edit_text
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
)
|
| 391 |
-
)
|
| 392 |
except MessageNotModified:
|
| 393 |
pass
|
| 394 |
try:
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 414 |
-
status_msg.edit_text
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 427 |
-
msg.reply_text
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
if msg.chat.type != enums.ChatType.PRIVATE and msg.from_user:
|
| 434 |
try:
|
| 435 |
-
|
| 436 |
-
bot.send_message
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 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 |
-
|
| 448 |
-
status_msg.edit_text
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
if hasattr(Var, 'BIN_CHANNEL') and isinstance(Var.BIN_CHANNEL, int) and Var.BIN_CHANNEL != 0:
|
| 34 |
-
tasks.append(
|
| 35 |
await asyncio.gather(*tasks, return_exceptions=True)
|
| 36 |
|
| 37 |
|
| 38 |
async def reply_user_err(msg: Message, err_txt: str):
|
| 39 |
-
|
| 40 |
-
msg.reply_text
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
elif qry.isdigit():
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
elif isinstance(qry, int):
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
return None
|
| 100 |
|
| 101 |
|
| 102 |
async def is_admin(cli: Client, chat_id_val: int) -> bool:
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 38 |
-
message.reply_text
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 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 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 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
|
|
|
|
| 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 =
|
| 12 |
self.db = self._client[database_name]
|
| 13 |
-
self.col:
|
| 14 |
-
self.banned_users_col:
|
| 15 |
-
self.banned_channels_col:
|
| 16 |
-
self.token_col:
|
| 17 |
-
self.authorized_users_col:
|
| 18 |
-
self.restart_message_col:
|
| 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 |
-
|
| 32 |
-
message.reply_text
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
return False
|
| 70 |
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
if not me:
|
| 73 |
logger.error(f"Failed to get bot info for user {user_id} in require_token.", exc_info=True)
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 87 |
-
message.reply_text
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 58 |
-
message.reply_text
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
else:
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
return False
|
| 67 |
except Exception as e:
|
| 68 |
logger.error(f"Error checking force channel: {e}", exc_info=True)
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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.
|
| 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.
|
| 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 |
-
|
| 394 |
-
bot.send_message
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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", "
|
| 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=
|
| 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="
|
| 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 |
-
|
| 6 |
psutil
|
| 7 |
python-dotenv
|
| 8 |
speedtest-cli
|
|
|
|
| 2 |
cloudscraper
|
| 3 |
Jinja2
|
| 4 |
pyrofork
|
| 5 |
+
pymongo
|
| 6 |
psutil
|
| 7 |
python-dotenv
|
| 8 |
speedtest-cli
|