Compare commits
10
Commits
db9e048353
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1b7ec98de | ||
|
|
fc53d6f073 | ||
|
|
fc16c47343 | ||
|
|
71eb469154 | ||
|
|
25fe871305 | ||
|
|
ffb3abd9f1 | ||
|
|
172e5255ac | ||
|
|
b7b92d6cd2 | ||
|
|
025fae4433 | ||
|
|
6a78f43fc9 |
@@ -0,0 +1,207 @@
|
|||||||
|
# 🚀 Tele2Rub
|
||||||
|
|
||||||
|
انتقال خودکار فایل از تلگرام به روبیکا — سریع، ساده، بدون هزینه و دردسر
|
||||||
|
|
||||||
|
**پشتیبانی از ارسال فایل تا ۲ گیگابایت**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 معرفی
|
||||||
|
|
||||||
|
**Tele2Rub**
|
||||||
|
|
||||||
|
یک ابزار سبک و کاربردی است که فایلها را از بات تلگرام دریافت کرده و به صورت خودکار به **سیو مسیج (Saved Messages)** روبیکا ارسال میکند.
|
||||||
|
|
||||||
|
کل فرایند به صورت **صف (Queue)** انجام میشود تا از بروز خطا و تداخل جلوگیری شود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ نحوه کار
|
||||||
|
|
||||||
|
```text
|
||||||
|
ارسال به روبیکا → صف پردازش → دانلود فایل → ربات تلگرام
|
||||||
|
```
|
||||||
|
|
||||||
|
* دریافت فایل از تلگرام
|
||||||
|
* ذخیره موقت در سرور
|
||||||
|
* ثبت در صف
|
||||||
|
* ارسال خودکار توسط **Worker**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ قابلیتها
|
||||||
|
|
||||||
|
* 📥 دریافت انواع فایل از تلگرام
|
||||||
|
* 🔗 پشتیبانی دانلود از لینک مستقیم
|
||||||
|
* 📤 ارسال خودکار به روبیکا
|
||||||
|
* 🧾 ارسال همه فایلها به صورت Document
|
||||||
|
* 📊 نمایش وضعیت دانلود و آپلود
|
||||||
|
* ❌ امکان حذف یک فایل از صف با `/del`
|
||||||
|
* 🧨 امکان پاکسازی کل صف با `/delall`
|
||||||
|
* 🛡 حالت Safe Mode برای ارسال فایل ها به صورت ZIP رمز دار
|
||||||
|
* ⚡سیستم صف برای جلوگیری از کرش
|
||||||
|
* 🔄 اجرای جداگانه پردازش برای پایداری بیشتر
|
||||||
|
* ⏱ محدودیت زمان آپلود برای جلوگیری از گیر کردن پردازش
|
||||||
|
(این مقدار با توجه به سرعت سرور، حجم فایلها و صبر و حوصله خودتون قابل تنظیمه.)
|
||||||
|
(بهصورت پیشفرض روی **۱۰ دقیقه** تنظیم شده.)
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 نصب سریع
|
||||||
|
|
||||||
|
ابتدا پروژه را دریافت کنید:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/caffeinexz/Tele2Rub.git
|
||||||
|
cd Tele2Rub
|
||||||
|
```
|
||||||
|
|
||||||
|
نصب وابستگیها:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
اجرای پروژه:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥 نصب روی سرور
|
||||||
|
|
||||||
|
### 1. نصب پیشنیازها
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3 python3-venv python3-pip git -y
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. دریافت پروژه
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/caffeinexz/Tele2Rub.git
|
||||||
|
cd Tele2Rub
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. ساخت محیط مجازی
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. فعالسازی محیط مجازی
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. نصب وابستگیها
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. ساخت فایل تنظیمات
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
و مقادیر زیر را وارد کنید:
|
||||||
|
|
||||||
|
```env
|
||||||
|
API_ID=عدد_API
|
||||||
|
API_HASH=کد_API
|
||||||
|
BOT_TOKEN=توکن_ربات
|
||||||
|
RUBIKA_SESSION=rubsession
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. اجرای دائمی (Screen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
screen -S tele2rub
|
||||||
|
source venv/bin/activate
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ تنظیمات
|
||||||
|
|
||||||
|
یک فایل `.env` در **ریشه پروژه** بسازید:
|
||||||
|
|
||||||
|
```env
|
||||||
|
API_ID=عدد_API
|
||||||
|
API_HASH=کد_API
|
||||||
|
BOT_TOKEN=توکن_ربات
|
||||||
|
RUBIKA_SESSION=rubsession
|
||||||
|
```
|
||||||
|
|
||||||
|
یا از فایل نمونه استفاده کنید:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📌 دریافت API_ID و API_HASH از تلگرام
|
||||||
|
|
||||||
|
برای استفاده از پروژه، ابتدا باید API تلگرام دریافت کنید:
|
||||||
|
|
||||||
|
1. وارد سایت زیر شوید:
|
||||||
|
👉 https://my.telegram.org
|
||||||
|
|
||||||
|
2. با شماره تلگرام خود وارد شوید
|
||||||
|
|
||||||
|
3. روی **API development tools** کلیک کنید
|
||||||
|
|
||||||
|
4. فرم را به شکل زیر پر کنید:
|
||||||
|
|
||||||
|
```text
|
||||||
|
App title: tele2rub
|
||||||
|
Short name: t2r
|
||||||
|
```
|
||||||
|
|
||||||
|
5. پس از ثبت، مقادیر زیر به شما داده میشود:
|
||||||
|
|
||||||
|
* **API_ID**
|
||||||
|
* **API_HASH**
|
||||||
|
|
||||||
|
این مقادیر را در فایل `.env` قرار دهید.
|
||||||
|
|
||||||
|
درصورت مشکل در دریافت API ID و API HASH مقادیر در کانال تلگرام قرار گرفته
|
||||||
|
لینک : https://t.me/caffeinexz/3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 اجرای اولیه
|
||||||
|
|
||||||
|
در اولین اجرا:
|
||||||
|
|
||||||
|
* شماره روبیکا را وارد کنید
|
||||||
|
* کد تایید را وارد کنید
|
||||||
|
* فایل سشن ذخیره میشود و در دفعات بعد نیاز نیست
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📥 نحوه استفاده
|
||||||
|
|
||||||
|
1. وارد **بات تلگرام** شوید
|
||||||
|
2. فایل ارسال کنید
|
||||||
|
3. فایل به صورت خودکار در **Saved Messages روبیکا** ارسال میشود
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# 🚀 Tele2Rub
|
||||||
|
|
||||||
|
انتقال خودکار فایل از تلگرام به روبیکا — سریع، ساده، بدون هزینه و دردسر
|
||||||
|
|
||||||
|
**پشتیبانی از ارسال فایل تا ۲ گیگابایت**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 معرفی
|
||||||
|
|
||||||
|
**Tele2Rub**
|
||||||
|
|
||||||
|
یک ابزار سبک و کاربردی است که فایلها را از بات تلگرام دریافت کرده و به صورت خودکار به **سیو مسیج (Saved Messages)** روبیکا ارسال میکند.
|
||||||
|
|
||||||
|
کل فرایند به صورت **صف (Queue)** انجام میشود تا از بروز خطا و تداخل جلوگیری شود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ نحوه کار
|
||||||
|
|
||||||
|
```text
|
||||||
|
ارسال به روبیکا → صف پردازش → دانلود فایل → ربات تلگرام
|
||||||
|
```
|
||||||
|
|
||||||
|
* دریافت فایل از تلگرام
|
||||||
|
* ذخیره موقت در سرور
|
||||||
|
* ثبت در صف
|
||||||
|
* ارسال خودکار توسط **Worker**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ قابلیتها
|
||||||
|
|
||||||
|
* 📥 دریافت انواع فایل از تلگرام
|
||||||
|
* 🔗 پشتیبانی دانلود از لینک مستقیم
|
||||||
|
* 📤 ارسال خودکار به روبیکا
|
||||||
|
* 🧾 ارسال همه فایلها به صورت Document
|
||||||
|
* 📊 نمایش وضعیت دانلود و آپلود
|
||||||
|
* ❌ امکان حذف یک فایل از صف با `/del`
|
||||||
|
* 🧨 امکان پاکسازی کل صف با `/delall`
|
||||||
|
* 🛡 حالت Safe Mode برای ارسال فایل ها به صورت ZIP رمز دار
|
||||||
|
* ⚡سیستم صف برای جلوگیری از کرش
|
||||||
|
* 🔄 اجرای جداگانه پردازش برای پایداری بیشتر
|
||||||
|
* ⏱ محدودیت زمان آپلود برای جلوگیری از گیر کردن پردازش
|
||||||
|
(این مقدار با توجه به سرعت سرور، حجم فایلها و صبر و حوصله خودتون قابل تنظیمه.)
|
||||||
|
(بهصورت پیشفرض روی **۱۰ دقیقه** تنظیم شده.)
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 نصب سریع
|
||||||
|
|
||||||
|
ابتدا پروژه را دریافت کنید:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/caffeinexz/Tele2Rub.git
|
||||||
|
cd Tele2Rub
|
||||||
|
```
|
||||||
|
|
||||||
|
نصب وابستگیها:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
اجرای پروژه:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥 نصب روی سرور
|
||||||
|
|
||||||
|
### 1. نصب پیشنیازها
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3 python3-venv python3-pip git -y
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. دریافت پروژه
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/caffeinexz/Tele2Rub.git
|
||||||
|
cd Tele2Rub
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. ساخت محیط مجازی
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. فعالسازی محیط مجازی
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. نصب وابستگیها
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. ساخت فایل تنظیمات
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
و مقادیر زیر را وارد کنید:
|
||||||
|
|
||||||
|
```env
|
||||||
|
API_ID=عدد_API
|
||||||
|
API_HASH=کد_API
|
||||||
|
BOT_TOKEN=توکن_ربات
|
||||||
|
RUBIKA_SESSION=rubsession
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. اجرای دائمی (Screen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
screen -S tele2rub
|
||||||
|
source venv/bin/activate
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ تنظیمات
|
||||||
|
|
||||||
|
یک فایل `.env` در **ریشه پروژه** بسازید:
|
||||||
|
|
||||||
|
```env
|
||||||
|
API_ID=عدد_API
|
||||||
|
API_HASH=کد_API
|
||||||
|
BOT_TOKEN=توکن_ربات
|
||||||
|
RUBIKA_SESSION=rubsession
|
||||||
|
```
|
||||||
|
|
||||||
|
یا از فایل نمونه استفاده کنید:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📌 دریافت API_ID و API_HASH از تلگرام
|
||||||
|
|
||||||
|
برای استفاده از پروژه، ابتدا باید API تلگرام دریافت کنید:
|
||||||
|
|
||||||
|
1. وارد سایت زیر شوید:
|
||||||
|
👉 https://my.telegram.org
|
||||||
|
|
||||||
|
2. با شماره تلگرام خود وارد شوید
|
||||||
|
|
||||||
|
3. روی **API development tools** کلیک کنید
|
||||||
|
|
||||||
|
4. فرم را به شکل زیر پر کنید:
|
||||||
|
|
||||||
|
```text
|
||||||
|
App title: tele2rub
|
||||||
|
Short name: t2r
|
||||||
|
```
|
||||||
|
|
||||||
|
5. پس از ثبت، مقادیر زیر به شما داده میشود:
|
||||||
|
|
||||||
|
* **API_ID**
|
||||||
|
* **API_HASH**
|
||||||
|
|
||||||
|
این مقادیر را در فایل `.env` قرار دهید.
|
||||||
|
|
||||||
|
درصورت مشکل در دریافت API ID و API HASH مقادیر در کانال تلگرام قرار گرفته
|
||||||
|
لینک : https://t.me/caffeinexz/3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 اجرای اولیه
|
||||||
|
|
||||||
|
در اولین اجرا:
|
||||||
|
|
||||||
|
* شماره روبیکا را وارد کنید
|
||||||
|
* کد تایید را وارد کنید
|
||||||
|
* فایل سشن ذخیره میشود و در دفعات بعد نیاز نیست
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📥 نحوه استفاده
|
||||||
|
|
||||||
|
1. وارد **بات تلگرام** شوید
|
||||||
|
2. فایل ارسال کنید
|
||||||
|
3. فایل به صورت خودکار در **Saved Messages روبیکا** ارسال میشود
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
API_ID=api_id
|
||||||
|
API_HASH=api_hash
|
||||||
|
BOT_TOKEN=bot_token
|
||||||
|
RUBIKA_SESSION=rubsession
|
||||||
|
|
||||||
|
# Optional Rubika upload tuning for large files
|
||||||
|
RUBIKA_REQUEST_TIMEOUT=180
|
||||||
|
RUBIKA_UPLOAD_RETRIES=8
|
||||||
|
RUBIKA_CHUNK_MB=1
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Repository Guidelines
|
||||||
|
|
||||||
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
|
This repository is a small Python service that bridges Telegram bot uploads to Rubika Saved Messages. Core code lives at the repository root:
|
||||||
|
|
||||||
|
- `main.py` starts both worker processes.
|
||||||
|
- `telebot.py` handles Telegram bot input, downloads, queue updates, and user commands.
|
||||||
|
- `rub.py` consumes queued tasks and uploads files to Rubika.
|
||||||
|
- `requirements.txt` lists runtime dependencies.
|
||||||
|
- `.env.example` documents required configuration keys.
|
||||||
|
|
||||||
|
Runtime state is written to `downloads/`, `queue/`, `*.session`, `*.session-journal`, `rubsession.rp`, and `out.log`. Treat these as local artifacts; do not commit real sessions, downloaded files, logs, or secrets.
|
||||||
|
|
||||||
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
|
Create and activate a virtual environment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Create local configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Run both Telegram and Rubika processes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Run individual components while debugging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 telebot.py
|
||||||
|
python3 rub.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
|
Use Python 3.10+ style. Follow the existing module pattern: constants in `UPPER_SNAKE_CASE`, functions and variables in `snake_case`, and classes in `PascalCase`. Keep paths based on `Path(__file__).resolve().parent` so scripts work from any current directory. Prefer JSONL queue records and UTF-8 file I/O, matching the current `queue/*.jsonl` format. Keep user-facing Persian status text readable and avoid changing message wording unless behavior changes require it.
|
||||||
|
|
||||||
|
## Testing Guidelines
|
||||||
|
|
||||||
|
There is currently no formal test suite. Before submitting changes, run the affected entry point locally with a disposable `.env` and verify the relevant queue flow. For queue logic, test with small files first, then direct URLs or larger files if upload behavior changed. If adding tests, place them under `tests/`, name files `test_*.py`, and prefer `pytest` for isolated helpers such as `safe_filename`, queue parsing, and retry decisions.
|
||||||
|
|
||||||
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
Recent history uses short imperative messages such as `Update README.md` and `fix disconnect session client when uploading`. Keep commits concise and focused, for example `fix empty caption handling` or `update queue cleanup`. Pull requests should include a short summary, manual test steps, affected commands, and any configuration or session implications. Include screenshots only when Telegram or Rubika message output changes.
|
||||||
|
|
||||||
|
## Security & Configuration Tips
|
||||||
|
|
||||||
|
Never commit `.env`, session files, downloaded media, or queue state containing user data. Required environment keys are `API_ID`, `API_HASH`, `BOT_TOKEN`, and `RUBIKA_SESSION`. Use `.env.example` for placeholders only.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
# Tele2Rub
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
**Tele2Rub** is a Python-based utility bot that automatically forwards files and direct download URLs from Telegram to a Rubika account's Saved Messages. It is designed with a lightweight queue system to prevent crashes and handle large files (up to 2GB) efficiently. The project is an optimized version of the original `caffeinexz/Tele2Rub`.
|
||||||
|
|
||||||
|
### Architecture & Components
|
||||||
|
- **`telebot.py`**: A Telegram bot built with `pyrogram`. It listens for incoming files or direct download links from the user, downloads the media locally to a `downloads/` folder, and registers the upload task into a local queue.
|
||||||
|
- **`rub.py`**: A worker script built with `rubpy`. It constantly polls the task queue, optionally processes the file (e.g., zipping it with a password if "Safe Mode" is enabled via `pyzipper`), and uploads the file to Rubika's Saved Messages.
|
||||||
|
- **`main.py`**: The main entry point that spawns and manages both the Telegram bot (`telebot.py`) and the Rubika worker (`rub.py`) as concurrent subprocesses.
|
||||||
|
- **Queue System**: Uses JSON Lines (`.jsonl`) files stored in the `queue/` directory (`tasks.jsonl`, `status.jsonl`, `cancelled.jsonl`, etc.) to track pending, processing, and failed uploads.
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Python 3.10+
|
||||||
|
- Telegram API ID and API Hash (from my.telegram.org)
|
||||||
|
- Telegram Bot Token (from BotFather)
|
||||||
|
- Rubika Session
|
||||||
|
|
||||||
|
### Setup Instructions
|
||||||
|
1. **Clone & Virtual Environment**:
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
2. **Install Dependencies**:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
3. **Configuration**:
|
||||||
|
Create a `.env` file in the project root with the following variables:
|
||||||
|
```env
|
||||||
|
API_ID=your_api_id
|
||||||
|
API_HASH=your_api_hash
|
||||||
|
BOT_TOKEN=your_bot_token
|
||||||
|
RUBIKA_SESSION=rubsession
|
||||||
|
```
|
||||||
|
4. **Run the Project**:
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
*(On the first run, the Rubika worker will prompt for your phone number and SMS code to generate the session file).*
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
- **Queue Files**: State is managed via text files in the `queue/` directory. Be careful with concurrent reads/writes; the system primarily relies on simple file appends and whole-file rewrites.
|
||||||
|
- **Safe Mode**: Users can toggle a safe mode that encrypts files into a password-protected `.zip` before uploading to Rubika, handled using `pyzipper`.
|
||||||
|
- **Error Handling**: The Rubika upload logic contains transient error handling (e.g., for 502/503 errors) and retries up to 3 times with exponential backoff.
|
||||||
|
- **Cleanups**: Downloaded and compressed files are cleaned up asynchronously upon successful upload or fatal error.
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# 🚀 Tele2Rub
|
||||||
|
|
||||||
|
انتقال خودکار فایل از تلگرام به روبیکا — سریع، ساده، بدون هزینه و دردسر
|
||||||
|
|
||||||
|
**پشتیبانی از ارسال فایل تا ۲ گیگابایت**
|
||||||
|
|
||||||
|
*** این یک نسخه بهینه شده از ریپازیتوری caffeinexz/Tele2Rub میباشد ***
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 معرفی
|
||||||
|
|
||||||
|
**Tele2Rub**
|
||||||
|
|
||||||
|
یک ابزار سبک و کاربردی است که فایلها را از بات تلگرام دریافت کرده و به صورت خودکار به **سیو مسیج (Saved Messages)** روبیکا ارسال میکند.
|
||||||
|
|
||||||
|
کل فرایند به صورت **صف (Queue)** انجام میشود تا از بروز خطا و تداخل جلوگیری شود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ نحوه کار
|
||||||
|
|
||||||
|
```text
|
||||||
|
ارسال به روبیکا → صف پردازش → دانلود فایل → ربات تلگرام
|
||||||
|
```
|
||||||
|
|
||||||
|
* دریافت فایل از تلگرام
|
||||||
|
* ذخیره موقت در سرور
|
||||||
|
* ثبت در صف
|
||||||
|
* ارسال خودکار توسط **Worker**
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔔🛠️ آپدیت
|
||||||
|
|
||||||
|
* حل باگ Caption:
|
||||||
|
- ` رفع خطای ERROR_GENERIC یا INVALID_INPUT که به دلیل ارسال کپشنهای خالی به API روبیکا رخ میداد.`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ قابلیتها
|
||||||
|
|
||||||
|
* 📥 دریافت انواع فایل از تلگرام
|
||||||
|
* 🔗 پشتیبانی دانلود از لینک مستقیم
|
||||||
|
* 📤 ارسال خودکار به روبیکا
|
||||||
|
* 🧾 ارسال همه فایلها به صورت Document
|
||||||
|
* 📊 نمایش وضعیت دانلود و آپلود
|
||||||
|
* ❌ امکان حذف یک فایل از صف با `/del`
|
||||||
|
* 🧨 امکان پاکسازی کل صف با `/delall`
|
||||||
|
* 🛡 حالت Safe Mode برای ارسال فایل ها به صورت ZIP رمز دار
|
||||||
|
* ⚡سیستم صف برای جلوگیری از کرش
|
||||||
|
* 🔄 اجرای جداگانه پردازش برای پایداری بیشتر
|
||||||
|
* ⏱ محدودیت زمان آپلود برای جلوگیری از گیر کردن پردازش
|
||||||
|
(این مقدار با توجه به سرعت سرور، حجم فایلها و صبر و حوصله خودتون قابل تنظیمه.)
|
||||||
|
(بهصورت پیشفرض روی **۱۰ دقیقه** تنظیم شده.)
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 نصب سریع
|
||||||
|
|
||||||
|
ابتدا پروژه را دریافت کنید:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/DayiGorbay/Tele2Rub.git
|
||||||
|
cd Tele2Rub
|
||||||
|
```
|
||||||
|
|
||||||
|
نصب وابستگیها:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
اجرای پروژه:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥 نصب روی سرور
|
||||||
|
|
||||||
|
### 1. نصب پیشنیازها
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install python3 python3-venv python3-pip git -y
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. دریافت پروژه
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/DayiGorbay/Tele2Rub.git
|
||||||
|
cd Tele2Rub
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. ساخت محیط مجازی
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv venv
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. فعالسازی محیط مجازی
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. نصب وابستگیها
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. ساخت فایل تنظیمات
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
و مقادیر زیر را وارد کنید:
|
||||||
|
|
||||||
|
```env
|
||||||
|
API_ID=عدد_API
|
||||||
|
API_HASH=کد_API
|
||||||
|
BOT_TOKEN=توکن_ربات
|
||||||
|
RUBIKA_SESSION=rubsession
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. اجرای دائمی (Screen)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
screen -S tele2rub
|
||||||
|
source venv/bin/activate
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ تنظیمات
|
||||||
|
|
||||||
|
یک فایل `.env` در **ریشه پروژه** بسازید:
|
||||||
|
|
||||||
|
```env
|
||||||
|
API_ID=عدد_API
|
||||||
|
API_HASH=کد_API
|
||||||
|
BOT_TOKEN=توکن_ربات
|
||||||
|
RUBIKA_SESSION=rubsession
|
||||||
|
```
|
||||||
|
|
||||||
|
یا از فایل نمونه استفاده کنید:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📌 دریافت API_ID و API_HASH از تلگرام
|
||||||
|
|
||||||
|
برای استفاده از پروژه، ابتدا باید API تلگرام دریافت کنید:
|
||||||
|
|
||||||
|
1. وارد سایت زیر شوید:
|
||||||
|
👉 https://my.telegram.org
|
||||||
|
|
||||||
|
2. با شماره تلگرام خود وارد شوید
|
||||||
|
|
||||||
|
3. روی **API development tools** کلیک کنید
|
||||||
|
|
||||||
|
4. فرم را به شکل زیر پر کنید:
|
||||||
|
|
||||||
|
```text
|
||||||
|
App title: tele2rub
|
||||||
|
Short name: t2r
|
||||||
|
```
|
||||||
|
|
||||||
|
5. پس از ثبت، مقادیر زیر به شما داده میشود:
|
||||||
|
|
||||||
|
* **API_ID**
|
||||||
|
* **API_HASH**
|
||||||
|
|
||||||
|
این مقادیر را در فایل `.env` قرار دهید.
|
||||||
|
|
||||||
|
درصورت مشکل در دریافت API ID و API HASH مقادیر در کانال تلگرام قرار گرفته
|
||||||
|
لینک : https://t.me/caffeinexz/3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 اجرای اولیه
|
||||||
|
|
||||||
|
در اولین اجرا:
|
||||||
|
|
||||||
|
* شماره روبیکا را وارد کنید
|
||||||
|
* کد تایید را وارد کنید
|
||||||
|
* فایل سشن ذخیره میشود و در دفعات بعد نیاز نیست
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📥 نحوه استفاده
|
||||||
|
|
||||||
|
1. وارد **بات تلگرام** شوید
|
||||||
|
2. فایل ارسال کنید
|
||||||
|
3. فایل به صورت خودکار در **Saved Messages روبیکا** ارسال میشود
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
telegram_file = BASE_DIR / "telebot.py"
|
||||||
|
rubika_file = BASE_DIR / "rub.py"
|
||||||
|
|
||||||
|
telegram_proc = None
|
||||||
|
rubika_proc = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
rubika_proc = subprocess.Popen([sys.executable, str(rubika_file)])
|
||||||
|
telegram_proc = subprocess.Popen([sys.executable, str(telegram_file)])
|
||||||
|
|
||||||
|
rubika_proc.wait()
|
||||||
|
telegram_proc.wait()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
for proc in [telegram_proc, rubika_proc]:
|
||||||
|
if proc and proc.poll() is None:
|
||||||
|
proc.terminate()
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from rubpy import Client as RubikaClient
|
||||||
|
import requests
|
||||||
|
import pyzipper
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
SESSION = os.getenv("RUBIKA_SESSION", "rubika_session").strip()
|
||||||
|
RUBIKA_REQUEST_TIMEOUT = int(os.getenv("RUBIKA_REQUEST_TIMEOUT", "180"))
|
||||||
|
RUBIKA_UPLOAD_RETRIES = int(os.getenv("RUBIKA_UPLOAD_RETRIES", "8"))
|
||||||
|
RUBIKA_CHUNK_SIZE = int(float(os.getenv("RUBIKA_CHUNK_MB", "1")) * 1024 * 1024)
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
DOWNLOAD_DIR = BASE_DIR / "downloads"
|
||||||
|
QUEUE_DIR = BASE_DIR / "queue"
|
||||||
|
QUEUE_FILE = QUEUE_DIR / "tasks.jsonl"
|
||||||
|
PROCESSING_FILE = QUEUE_DIR / "processing.json"
|
||||||
|
FAILED_FILE = QUEUE_DIR / "failed.jsonl"
|
||||||
|
STATUS_FILE = QUEUE_DIR / "status.jsonl"
|
||||||
|
URL_DIR = DOWNLOAD_DIR / "url"
|
||||||
|
CANCEL_FILE = QUEUE_DIR / "cancelled.jsonl"
|
||||||
|
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
TARGET = "me"
|
||||||
|
|
||||||
|
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
QUEUE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
URL_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_filename(name: Optional[str]) -> str:
|
||||||
|
name = (name or "file").strip()
|
||||||
|
name = re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name)
|
||||||
|
name = name.rstrip(". ")
|
||||||
|
return name[:200] or "file"
|
||||||
|
|
||||||
|
def pretty_size(size) -> str:
|
||||||
|
size = float(size or 0)
|
||||||
|
units = ["B", "KB", "MB", "GB"]
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
while size >= 1024 and index < len(units) - 1:
|
||||||
|
size /= 1024
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return f"{size:.2f} {units[index]}"
|
||||||
|
|
||||||
|
def eta_text(seconds) -> str:
|
||||||
|
if not seconds or seconds <= 0:
|
||||||
|
return "نامشخص"
|
||||||
|
|
||||||
|
seconds = int(seconds)
|
||||||
|
h = seconds // 3600
|
||||||
|
m = (seconds % 3600) // 60
|
||||||
|
s = seconds % 60
|
||||||
|
|
||||||
|
if h:
|
||||||
|
return f"{h}h {m}m"
|
||||||
|
if m:
|
||||||
|
return f"{m}m {s}s"
|
||||||
|
return f"{s}s"
|
||||||
|
|
||||||
|
|
||||||
|
def push_status(task: dict, text: str, status: str = "working", percent: float | None = None):
|
||||||
|
payload = {
|
||||||
|
"chat_id": task.get("chat_id"),
|
||||||
|
"message_id": task.get("status_message_id"),
|
||||||
|
"status": status,
|
||||||
|
"text": text,
|
||||||
|
"percent": percent,
|
||||||
|
"time": time.time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(STATUS_FILE, "a", encoding="utf-8") as file:
|
||||||
|
file.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
def is_cancelled(task: dict) -> bool:
|
||||||
|
job_id = str(task.get("job_id", ""))
|
||||||
|
|
||||||
|
if not job_id or not CANCEL_FILE.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(CANCEL_FILE, "r", encoding="utf-8") as file:
|
||||||
|
for line in file:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
item = json.loads(line)
|
||||||
|
if str(item.get("job_id")) == job_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unique_path(path: Path) -> Path:
|
||||||
|
if not path.exists():
|
||||||
|
return path
|
||||||
|
|
||||||
|
stem = path.stem
|
||||||
|
suffix = path.suffix
|
||||||
|
index = 1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
candidate = path.with_name(f"{stem}_{index}{suffix}")
|
||||||
|
if not candidate.exists():
|
||||||
|
return candidate
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
|
||||||
|
def has_session(session_name: str) -> bool:
|
||||||
|
candidates = [
|
||||||
|
Path(session_name),
|
||||||
|
Path(f"{session_name}.session"),
|
||||||
|
Path(f"{session_name}.sqlite"),
|
||||||
|
]
|
||||||
|
return any(path.exists() for path in candidates)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_session():
|
||||||
|
if has_session(SESSION):
|
||||||
|
return
|
||||||
|
|
||||||
|
client = RubikaClient(name=SESSION)
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.start()
|
||||||
|
print("Login successful.")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
client.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def build_upload_text(file_name: str, total: int, current: int, started_at: float) -> tuple[str, float]:
|
||||||
|
now = time.time()
|
||||||
|
percent = current * 100 / total if total else 0
|
||||||
|
elapsed = max(now - started_at, 1)
|
||||||
|
speed = current / elapsed
|
||||||
|
eta = (total - current) / speed if total and speed else None
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"🔼 در حال آپلود در روبیکا...\n\n"
|
||||||
|
f"فایل: `{file_name}`\n"
|
||||||
|
f"آپلود شده: `{pretty_size(current)}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
if total:
|
||||||
|
text += f" از `{pretty_size(total)}`"
|
||||||
|
|
||||||
|
text += (
|
||||||
|
f"\nسرعت: `{pretty_size(speed)}/s`\n"
|
||||||
|
f"زمان باقیمانده: `{eta_text(eta)}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
return text, percent
|
||||||
|
|
||||||
|
|
||||||
|
def send_document_streaming(file_path: str, caption: str | None = None, task: dict | None = None):
|
||||||
|
path = Path(file_path)
|
||||||
|
client = RubikaClient(
|
||||||
|
name=SESSION,
|
||||||
|
timeout=RUBIKA_REQUEST_TIMEOUT,
|
||||||
|
max_retries=RUBIKA_UPLOAD_RETRIES,
|
||||||
|
)
|
||||||
|
progress_state = {"last_update": 0}
|
||||||
|
started_at = time.time()
|
||||||
|
|
||||||
|
def upload_progress(total: int, current: int):
|
||||||
|
if not task:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now - progress_state["last_update"] < 5 and current < total:
|
||||||
|
return
|
||||||
|
|
||||||
|
progress_state["last_update"] = now
|
||||||
|
text, percent = build_upload_text(path.name, total, current, started_at)
|
||||||
|
push_status(task, text, "uploading", percent)
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.start()
|
||||||
|
|
||||||
|
uploaded = client.upload(
|
||||||
|
str(path),
|
||||||
|
chunk=RUBIKA_CHUNK_SIZE,
|
||||||
|
callback=upload_progress,
|
||||||
|
backoff=2.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if task and is_cancelled(task):
|
||||||
|
raise RuntimeError("ارسال لغو شد.")
|
||||||
|
|
||||||
|
uploaded["type"] = "File"
|
||||||
|
uploaded["time"] = 1
|
||||||
|
uploaded["width"] = 200
|
||||||
|
uploaded["height"] = 200
|
||||||
|
uploaded["music_performer"] = ""
|
||||||
|
|
||||||
|
return client.send_message(
|
||||||
|
object_guid=TARGET,
|
||||||
|
text=caption,
|
||||||
|
file_inline=uploaded,
|
||||||
|
thumb=False,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
client.disconnect()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_with_retry(file_path: str, caption: str = "", task: dict | None = None):
|
||||||
|
last_error = None
|
||||||
|
file_size = Path(file_path).stat().st_size
|
||||||
|
|
||||||
|
for attempt in range(1, MAX_RETRIES + 1):
|
||||||
|
if task and is_cancelled(task):
|
||||||
|
raise RuntimeError("ارسال لغو شد.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if task:
|
||||||
|
push_status(
|
||||||
|
task,
|
||||||
|
f"🔼 در حال آپلود در روبیکا...\n\n"
|
||||||
|
f"حجم: `{pretty_size(file_size)}`\n"
|
||||||
|
f"🔴 تلاش {attempt} از {MAX_RETRIES}\n\n"
|
||||||
|
f"برای لغو ارسال:\n"
|
||||||
|
f"`/del {task.get('job_id')}`",
|
||||||
|
"uploading"
|
||||||
|
)
|
||||||
|
|
||||||
|
return send_document_streaming(file_path, caption, task)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
error_text = str(e).lower()
|
||||||
|
|
||||||
|
transient = any(
|
||||||
|
key in error_text
|
||||||
|
for key in [
|
||||||
|
"502", "503", "bad gateway", "timeout",
|
||||||
|
"cannot connect", "connection reset",
|
||||||
|
"temporarily unavailable",
|
||||||
|
"error uploading chunk",
|
||||||
|
"unexpected mimetype",
|
||||||
|
"server disconnected",
|
||||||
|
"try again",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if transient and attempt < MAX_RETRIES:
|
||||||
|
|
||||||
|
if task and is_cancelled(task):
|
||||||
|
raise RuntimeError("ارسال لغو شد.")
|
||||||
|
|
||||||
|
if task:
|
||||||
|
push_status(
|
||||||
|
task,
|
||||||
|
f"ارتباط با روبیکا ناپایدار بود...\n"
|
||||||
|
f"دوباره تلاش میکنم ({attempt + 1} از {MAX_RETRIES})",
|
||||||
|
"uploading"
|
||||||
|
)
|
||||||
|
|
||||||
|
time.sleep(min(30, 5 * attempt))
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
raise last_error if last_error else RuntimeError("Upload failed.")
|
||||||
|
|
||||||
|
def download_url(task: dict) -> Path:
|
||||||
|
url = task.get("url", "").strip()
|
||||||
|
if not url:
|
||||||
|
raise RuntimeError("URL خالیه")
|
||||||
|
|
||||||
|
push_status(task, "در حال دانلود ...", "downloading", 0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, stream=True, timeout=(10, 60), allow_redirects=True)
|
||||||
|
resp.raise_for_status()
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
raise RuntimeError("لینک جواب نداد")
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
raise RuntimeError("مشکل شبکه")
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
code = e.response.status_code if e.response else "نامشخص"
|
||||||
|
raise RuntimeError(f"دانلود انجام نشد. کد خطا: {code}")
|
||||||
|
|
||||||
|
cd = resp.headers.get("content-disposition", "")
|
||||||
|
match = re.findall(r'filename="(.+?)"', cd)
|
||||||
|
name = match[0] if match else Path(urlparse(url).path).name
|
||||||
|
name = safe_filename(name or f"file_{int(time.time())}")
|
||||||
|
if "." not in name:
|
||||||
|
name += ".bin"
|
||||||
|
|
||||||
|
target = unique_path(URL_DIR / name)
|
||||||
|
total = int(resp.headers.get("content-length") or 0)
|
||||||
|
downloaded, last_update, started = 0, 0, time.time()
|
||||||
|
|
||||||
|
with open(target, "wb") as f:
|
||||||
|
for chunk in resp.iter_content(1024 * 1024):
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
f.write(chunk)
|
||||||
|
downloaded += len(chunk)
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if now - last_update < 3 and downloaded < total:
|
||||||
|
continue
|
||||||
|
last_update = now
|
||||||
|
|
||||||
|
speed = downloaded / max(now - started, 1)
|
||||||
|
eta = (total - downloaded) / speed if total and speed else None
|
||||||
|
percent = downloaded * 100 / total if total else None
|
||||||
|
|
||||||
|
text = f"داره دانلود میکنه...\n\n{pretty_size(downloaded)}"
|
||||||
|
if total:
|
||||||
|
text += f" از {pretty_size(total)}"
|
||||||
|
text += f"\nسرعت: {pretty_size(speed)}/s"
|
||||||
|
if eta:
|
||||||
|
text += f"\nمونده: {eta_text(eta)}"
|
||||||
|
|
||||||
|
push_status(task, text, "downloading", percent)
|
||||||
|
|
||||||
|
if not target.exists() or target.stat().st_size == 0:
|
||||||
|
raise RuntimeError("فایل دانلود نشد")
|
||||||
|
|
||||||
|
task["file_name"] = target.name
|
||||||
|
task["file_size"] = target.stat().st_size
|
||||||
|
return target
|
||||||
|
|
||||||
|
def make_zip_with_password(file_path: Path, password: str) -> Path:
|
||||||
|
zip_path = unique_path(file_path.with_suffix(file_path.suffix + ".zip"))
|
||||||
|
|
||||||
|
with pyzipper.AESZipFile(
|
||||||
|
zip_path,
|
||||||
|
"w",
|
||||||
|
compression=pyzipper.ZIP_STORED,
|
||||||
|
encryption=pyzipper.WZ_AES,
|
||||||
|
) as zip_file:
|
||||||
|
zip_file.setpassword(password.encode("utf-8"))
|
||||||
|
zip_file.write(file_path, arcname=file_path.name)
|
||||||
|
|
||||||
|
return zip_path
|
||||||
|
|
||||||
|
def pop_first_task():
|
||||||
|
if not QUEUE_FILE.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(QUEUE_FILE, "r", encoding="utf-8") as file:
|
||||||
|
lines = [line for line in file if line.strip()]
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
return None
|
||||||
|
|
||||||
|
first_line = lines[0]
|
||||||
|
remaining = lines[1:]
|
||||||
|
|
||||||
|
with open(QUEUE_FILE, "w", encoding="utf-8") as file:
|
||||||
|
file.writelines(remaining)
|
||||||
|
|
||||||
|
return json.loads(first_line)
|
||||||
|
|
||||||
|
|
||||||
|
def save_processing(task: dict) -> None:
|
||||||
|
with open(PROCESSING_FILE, "w", encoding="utf-8") as file:
|
||||||
|
json.dump(task, file, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_processing() -> None:
|
||||||
|
if PROCESSING_FILE.exists():
|
||||||
|
PROCESSING_FILE.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def append_failed(task: dict, error: str) -> None:
|
||||||
|
payload = {"task": task, "error": error}
|
||||||
|
with open(FAILED_FILE, "a", encoding="utf-8") as file:
|
||||||
|
file.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
def process_task(task: dict):
|
||||||
|
task_type = task.get("type")
|
||||||
|
caption = (task.get("caption") or "").strip()[:512]
|
||||||
|
if not caption:
|
||||||
|
caption = None
|
||||||
|
safe_mode = task.get("safe_mode", False)
|
||||||
|
zip_password = task.get("zip_password", "")
|
||||||
|
|
||||||
|
local_path: Path | None = None
|
||||||
|
|
||||||
|
if task_type == "local_file":
|
||||||
|
local_path = Path(task.get("path", ""))
|
||||||
|
|
||||||
|
if not local_path.exists():
|
||||||
|
raise RuntimeError("Local file not found.")
|
||||||
|
|
||||||
|
elif task_type == "direct_url":
|
||||||
|
local_path = download_url(task)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unknown task type.")
|
||||||
|
|
||||||
|
if safe_mode and zip_password:
|
||||||
|
push_status(task, "در حال تبدیل به فایل zip ...", "processing")
|
||||||
|
|
||||||
|
try:
|
||||||
|
zipped = make_zip_with_password(local_path, zip_password)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
if local_path.exists():
|
||||||
|
local_path.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
send_path = zipped
|
||||||
|
|
||||||
|
else:
|
||||||
|
send_path = local_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
if is_cancelled(task):
|
||||||
|
raise RuntimeError("ارسال لغو شد.")
|
||||||
|
|
||||||
|
send_with_retry(str(send_path), caption, task)
|
||||||
|
|
||||||
|
push_status(
|
||||||
|
task,
|
||||||
|
"فایل با موفقیت در روبیکا آپلود شد.",
|
||||||
|
"done"
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
if send_path and send_path.exists():
|
||||||
|
send_path.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def worker_loop():
|
||||||
|
ensure_session()
|
||||||
|
print("Rubika worker started.")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
task = pop_first_task()
|
||||||
|
|
||||||
|
if not task:
|
||||||
|
time.sleep(0.2)
|
||||||
|
continue
|
||||||
|
|
||||||
|
save_processing(task)
|
||||||
|
|
||||||
|
try:
|
||||||
|
process_task(task)
|
||||||
|
except Exception as e:
|
||||||
|
append_failed(task, str(e))
|
||||||
|
push_status(task, f"خطا: {str(e)}", "failed")
|
||||||
|
finally:
|
||||||
|
clear_processing()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
worker_loop()
|
||||||
@@ -0,0 +1,613 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from pyrogram import Client, filters
|
||||||
|
from pyrogram.types import Message
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from pyrogram import idle
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
API_ID = int(os.getenv("API_ID", "0"))
|
||||||
|
API_HASH = os.getenv("API_HASH", "").strip()
|
||||||
|
BOT_TOKEN = os.getenv("BOT_TOKEN", "").strip()
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
DOWNLOAD_DIR = BASE_DIR / "downloads"
|
||||||
|
QUEUE_DIR = BASE_DIR / "queue"
|
||||||
|
QUEUE_FILE = QUEUE_DIR / "tasks.jsonl"
|
||||||
|
STATUS_FILE = QUEUE_DIR / "status.jsonl"
|
||||||
|
SETTINGS_FILE = QUEUE_DIR / "settings.json"
|
||||||
|
DELETED_FILE = QUEUE_DIR / "deleted.jsonl"
|
||||||
|
CANCEL_FILE = QUEUE_DIR / "cancelled.jsonl"
|
||||||
|
|
||||||
|
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
QUEUE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not API_ID or not API_HASH or not BOT_TOKEN:
|
||||||
|
raise RuntimeError("Please set API_ID, API_HASH and BOT_TOKEN in .env")
|
||||||
|
|
||||||
|
app = Client(
|
||||||
|
"tel2rub",
|
||||||
|
api_id=API_ID,
|
||||||
|
api_hash=API_HASH,
|
||||||
|
bot_token=BOT_TOKEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_filename(name: Optional[str]) -> str:
|
||||||
|
name = (name or "file.bin").strip()
|
||||||
|
name = re.sub(r'[<>:"/\\|?*\x00-\x1F]', "_", name)
|
||||||
|
name = name.rstrip(". ")
|
||||||
|
return name[:200] or "file.bin"
|
||||||
|
|
||||||
|
|
||||||
|
def split_name(filename: str) -> tuple[str, str]:
|
||||||
|
path = Path(filename)
|
||||||
|
return path.stem, path.suffix
|
||||||
|
|
||||||
|
|
||||||
|
def get_media(message: Message):
|
||||||
|
media_types = [
|
||||||
|
("document", message.document),
|
||||||
|
("video", message.video),
|
||||||
|
("audio", message.audio),
|
||||||
|
("voice", message.voice),
|
||||||
|
("photo", message.photo),
|
||||||
|
("animation", message.animation),
|
||||||
|
("video_note", message.video_note),
|
||||||
|
("sticker", message.sticker),
|
||||||
|
]
|
||||||
|
|
||||||
|
for media_type, media in media_types:
|
||||||
|
if media:
|
||||||
|
return media_type, media
|
||||||
|
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def build_download_filename(message: Message, media_type: str, media) -> str:
|
||||||
|
original_name = getattr(media, "file_name", None)
|
||||||
|
|
||||||
|
if not original_name:
|
||||||
|
file_unique_id = getattr(media, "file_unique_id", None) or "file"
|
||||||
|
|
||||||
|
default_extensions = {
|
||||||
|
"document": ".bin",
|
||||||
|
"video": ".mp4",
|
||||||
|
"audio": ".mp3",
|
||||||
|
"voice": ".ogg",
|
||||||
|
"photo": ".jpg",
|
||||||
|
"animation": ".mp4",
|
||||||
|
"video_note": ".mp4",
|
||||||
|
"sticker": ".webp",
|
||||||
|
}
|
||||||
|
|
||||||
|
original_name = f"{file_unique_id}{default_extensions.get(media_type, '.bin')}"
|
||||||
|
|
||||||
|
original_name = safe_filename(original_name)
|
||||||
|
stem, suffix = split_name(original_name)
|
||||||
|
|
||||||
|
unique_name = f"{stem}_{message.id}{suffix or '.bin'}"
|
||||||
|
return safe_filename(unique_name)
|
||||||
|
|
||||||
|
waiting_for_zip_password = False
|
||||||
|
|
||||||
|
class QueueManager:
|
||||||
|
def __init__(self):
|
||||||
|
self._cache = None
|
||||||
|
self._mtime = 0
|
||||||
|
|
||||||
|
def all(self):
|
||||||
|
mtime = QUEUE_FILE.stat().st_mtime if QUEUE_FILE.exists() else 0
|
||||||
|
if mtime == self._mtime and self._cache is not None:
|
||||||
|
return self._cache
|
||||||
|
self._cache = []
|
||||||
|
if QUEUE_FILE.exists():
|
||||||
|
with open(QUEUE_FILE, "r", encoding="utf-8") as f:
|
||||||
|
self._cache = [json.loads(l) for l in f if l.strip()]
|
||||||
|
self._mtime = mtime
|
||||||
|
return self._cache
|
||||||
|
|
||||||
|
def push(self, task):
|
||||||
|
task.setdefault("job_id", str(int(time.time() * 1000)))
|
||||||
|
with open(QUEUE_FILE, "a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(task, ensure_ascii=False) + "\n")
|
||||||
|
self._cache = None
|
||||||
|
|
||||||
|
def remove(self, job_id=None, message_id=None):
|
||||||
|
tasks = self.all()
|
||||||
|
kept, removed = [], None
|
||||||
|
for t in tasks:
|
||||||
|
if (job_id and str(t.get("job_id")) == str(job_id)) or \
|
||||||
|
(message_id and int(t.get("status_message_id", 0)) == message_id):
|
||||||
|
removed = t
|
||||||
|
else:
|
||||||
|
kept.append(t)
|
||||||
|
if removed:
|
||||||
|
with open(QUEUE_FILE, "w", encoding="utf-8") as f:
|
||||||
|
f.writelines(json.dumps(t, ensure_ascii=False) + "\n" for t in kept)
|
||||||
|
self._cache = None
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
queue = QueueManager()
|
||||||
|
|
||||||
|
def mark_deleted(task: dict):
|
||||||
|
with open(DELETED_FILE, "a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(task, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
def mark_cancelled(task: dict):
|
||||||
|
with open(CANCEL_FILE, "a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(task, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
def cancel_job(job_id: str):
|
||||||
|
with open(CANCEL_FILE, "a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps({"job_id": str(job_id)}, ensure_ascii=False) + "\n")
|
||||||
|
|
||||||
|
def was_deleted(job_id=None, message_id=None) -> bool:
|
||||||
|
if not DELETED_FILE.exists():
|
||||||
|
return False
|
||||||
|
with open(DELETED_FILE, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
item = json.loads(line)
|
||||||
|
if job_id and str(item.get("job_id")) == str(job_id):
|
||||||
|
return True
|
||||||
|
if message_id and int(item.get("status_message_id", 0)) == message_id:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def load_settings() -> dict:
|
||||||
|
try:
|
||||||
|
if SETTINGS_FILE.exists():
|
||||||
|
return json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {"safe_mode": False, "zip_password": ""}
|
||||||
|
|
||||||
|
def save_settings(data: dict):
|
||||||
|
SETTINGS_FILE.write_text(
|
||||||
|
json.dumps(data, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_direct_url(text: str) -> bool:
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
url = extract_first_url(text)
|
||||||
|
if not url:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_first_url(text: str) -> Optional[str]:
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
|
||||||
|
match = re.search(r"https?://\S+", text)
|
||||||
|
return match.group(0) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def progress_bar(percent: float, length: int = 12) -> str:
|
||||||
|
filled = int(length * percent / 100)
|
||||||
|
return "█" * filled + "░" * (length - filled)
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_size(size) -> str:
|
||||||
|
size = float(size or 0)
|
||||||
|
units = ["B", "KB", "MB", "GB"]
|
||||||
|
|
||||||
|
index = 0
|
||||||
|
while size >= 1024 and index < len(units) - 1:
|
||||||
|
size /= 1024
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return f"{size:.2f} {units[index]}"
|
||||||
|
|
||||||
|
|
||||||
|
def eta_text(seconds) -> str:
|
||||||
|
if not seconds or seconds <= 0:
|
||||||
|
return "نامشخص"
|
||||||
|
|
||||||
|
seconds = int(seconds)
|
||||||
|
h = seconds // 3600
|
||||||
|
m = (seconds % 3600) // 60
|
||||||
|
s = seconds % 60
|
||||||
|
|
||||||
|
if h:
|
||||||
|
return f"{h}h {m}m"
|
||||||
|
if m:
|
||||||
|
return f"{m}m {s}s"
|
||||||
|
return f"{s}s"
|
||||||
|
|
||||||
|
|
||||||
|
async def download_progress(current, total, status_message, file_name, started_at, state):
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
if now - state.get("last_update", 0) < 3 and current < total:
|
||||||
|
return
|
||||||
|
|
||||||
|
state["last_update"] = now
|
||||||
|
|
||||||
|
percent = current * 100 / total if total else 0
|
||||||
|
elapsed = max(now - started_at, 1)
|
||||||
|
speed = current / elapsed
|
||||||
|
eta = (total - current) / speed if speed else None
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"📥 در حال دریافت فایل از تلگرام\n\n"
|
||||||
|
f"فایل: `{file_name}`\n"
|
||||||
|
f"حجم: `{pretty_size(total)}`\n"
|
||||||
|
f"پیشرفت: `{percent:.1f}%`\n"
|
||||||
|
f"`{progress_bar(percent)}`\n"
|
||||||
|
f"سرعت: `{pretty_size(speed)}/s`\n"
|
||||||
|
f"زمان باقیمانده: `{eta_text(eta)}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await status_message.edit_text(text)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def status_watcher():
|
||||||
|
pos = 0
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
if not STATUS_FILE.exists():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
with open(STATUS_FILE, "r", encoding="utf-8") as f:
|
||||||
|
f.seek(pos)
|
||||||
|
lines = f.readlines()
|
||||||
|
pos = f.tell()
|
||||||
|
for line in lines:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
data = json.loads(line)
|
||||||
|
chat_id = data.get("chat_id")
|
||||||
|
msg_id = data.get("message_id")
|
||||||
|
text = data.get("text", "")
|
||||||
|
percent = data.get("percent")
|
||||||
|
if not chat_id or not msg_id:
|
||||||
|
continue
|
||||||
|
if percent is not None:
|
||||||
|
text += f"\n\n`{progress_bar(float(percent))}` `{float(percent):.1f}%`"
|
||||||
|
try:
|
||||||
|
await app.edit_message_text(chat_id, msg_id, text)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app.on_message(filters.private & filters.command("start"))
|
||||||
|
async def start_handler(client: Client, message: Message):
|
||||||
|
await message.reply_text(
|
||||||
|
"سلام به ربات tele2rub خوش اومدی💙\n\n"
|
||||||
|
"برای انتقال فایل از تلگرام به روبیکا، کافیه فایل رو برام فوروارد کنی.\n"
|
||||||
|
"اگه لینک مستقیم دانلود هم بدی، فایل رو برات دانلود میکنم و توی روبیکا میفرستم.\n"
|
||||||
|
"⚠️لطفا فایلها رو حداکثر ۱۰ تا ۱۰ تا ارسال کن تا از سمت روبیکا به مشکل نخوره.\n\n"
|
||||||
|
"برای دانلود از یوتیوب، اینستاگرام و... از این ربات استفاده کن: @Gozilla_bot\n"
|
||||||
|
"بعد فایل رو اینجا بفرست تا توی روبیکا برات ارسال کنم.\n\n"
|
||||||
|
"📌 راهنمای ربات:\n\n"
|
||||||
|
"-حذف از صف:\n"
|
||||||
|
"هر فایل وقتی تو صف قرار میگیره یه دستور لغو داره که با اون دستور میتونی حذفش کنی.\n"
|
||||||
|
"⚠️اگر فایل در حال آپلود باشه، لغو بعد از پایان تلاش فعلی انجام میشه.\n\n"
|
||||||
|
"-پاکسازی کل صف:\n"
|
||||||
|
"/delall\n\n"
|
||||||
|
"-حالت Safe Mode:\n"
|
||||||
|
"همه فایلها با رمز دلخواهت به صورت ZIP رمزدار ارسال میشن.\n\n"
|
||||||
|
"برای فعال/غیرفعال کردن:\n"
|
||||||
|
" /safemode on\n"
|
||||||
|
" /safemode off\n\n"
|
||||||
|
"⚠️برای فایلهای حجیم و ویدیوها بهتره Safe Mode خاموش باشه تا سریعتر آپلود بشن.\n\n"
|
||||||
|
"@caffeinexz"
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.on_message(filters.private & filters.command("safemode"))
|
||||||
|
async def safemode_handler(client: Client, message: Message):
|
||||||
|
global waiting_for_zip_password
|
||||||
|
|
||||||
|
args = message.text.split(maxsplit=1)
|
||||||
|
|
||||||
|
if len(args) < 2:
|
||||||
|
await message.reply_text("برای تغییر وضعیت Safe Mode از `/safemode on` یا `/safemode off` استفاده کن.")
|
||||||
|
return
|
||||||
|
|
||||||
|
action = args[1].strip().lower()
|
||||||
|
settings = load_settings()
|
||||||
|
|
||||||
|
if action == "on":
|
||||||
|
settings["safe_mode"] = True
|
||||||
|
save_settings(settings)
|
||||||
|
waiting_for_zip_password = True
|
||||||
|
|
||||||
|
await message.reply_text(
|
||||||
|
"Safe Mode فعال شد.\n\n"
|
||||||
|
"لطفا رمزی که میخواهید روی فایلهای ZIP قرار بگیرد را ارسال کنید.\n"
|
||||||
|
"از این به بعد فایلها قبل از ارسال به روبیکا با همین رمز ZIP میشوند."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if action == "off":
|
||||||
|
settings["safe_mode"] = False
|
||||||
|
settings["zip_password"] = ""
|
||||||
|
save_settings(settings)
|
||||||
|
waiting_for_zip_password = False
|
||||||
|
|
||||||
|
await message.reply_text(
|
||||||
|
"Safe Mode غیرفعال شد.\n\n"
|
||||||
|
"از این به بعد فایلها بهصورت عادی ارسال میشوند."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.reply_text("دستور نامعتبر است. از `/safemode on` یا `/safemode off` استفاده کن.")
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_message(filters.private & filters.command("delall"))
|
||||||
|
async def clear_queue_handler(client: Client, message: Message):
|
||||||
|
tasks = queue.all()
|
||||||
|
|
||||||
|
if not tasks:
|
||||||
|
await message.reply_text("صف خالی است.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
mark_deleted(task)
|
||||||
|
|
||||||
|
old_path = task.get("path")
|
||||||
|
if old_path:
|
||||||
|
try:
|
||||||
|
path = Path(old_path)
|
||||||
|
if path.exists():
|
||||||
|
path.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.edit_message_text(
|
||||||
|
chat_id=task["chat_id"],
|
||||||
|
message_id=task["status_message_id"],
|
||||||
|
text="این مورد از صف حذف شد."
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with open(QUEUE_FILE, "w", encoding="utf-8") as f:
|
||||||
|
pass
|
||||||
|
queue._cache = None
|
||||||
|
queue._mtime = 0
|
||||||
|
await message.reply_text("تمام موارد در صف پاک شد.")
|
||||||
|
|
||||||
|
@app.on_message(filters.private & filters.command("del"))
|
||||||
|
async def delete_one_handler(client: Client, message: Message):
|
||||||
|
job_id = None
|
||||||
|
reply_message_id = None
|
||||||
|
|
||||||
|
parts = message.text.split(maxsplit=1)
|
||||||
|
if len(parts) > 1:
|
||||||
|
job_id = parts[1].strip()
|
||||||
|
|
||||||
|
if message.reply_to_message:
|
||||||
|
reply_message_id = message.reply_to_message.id
|
||||||
|
|
||||||
|
tasks = queue.all()
|
||||||
|
|
||||||
|
if not tasks:
|
||||||
|
if job_id and was_deleted(job_id=job_id):
|
||||||
|
await message.reply_text("این مورد قبلاً از صف حذف شده است.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if reply_message_id and was_deleted(message_id=reply_message_id):
|
||||||
|
await message.reply_text("این مورد قبلاً از صف حذف شده است.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if job_id:
|
||||||
|
cancel_job(job_id)
|
||||||
|
await message.reply_text(
|
||||||
|
"لغو ثبت شد.\n\n"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await message.reply_text("موردی برای حذف در صف پیدا نشد.")
|
||||||
|
return
|
||||||
|
|
||||||
|
removed = queue.remove(job_id=job_id, message_id=reply_message_id)
|
||||||
|
|
||||||
|
if removed:
|
||||||
|
mark_deleted(removed)
|
||||||
|
|
||||||
|
old_path = removed.get("path")
|
||||||
|
if old_path:
|
||||||
|
try:
|
||||||
|
path = Path(old_path)
|
||||||
|
if path.exists():
|
||||||
|
path.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.edit_message_text(
|
||||||
|
chat_id=removed["chat_id"],
|
||||||
|
message_id=removed["status_message_id"],
|
||||||
|
text="این مورد از صف حذف شد."
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await message.reply_text("از صف حذف شد.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if job_id and was_deleted(job_id=job_id):
|
||||||
|
await message.reply_text("این مورد قبلاً از صف حذف شده است.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if reply_message_id and was_deleted(message_id=reply_message_id):
|
||||||
|
await message.reply_text("این مورد قبلاً از صف حذف شده است.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if job_id:
|
||||||
|
cancel_job(job_id)
|
||||||
|
await message.reply_text("دستور لغو ثبت شد.")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_message(filters.private & filters.text & ~filters.command(["start", "safemode", "del", "delall"]))
|
||||||
|
async def text_handler(client: Client, message: Message):
|
||||||
|
global waiting_for_zip_password
|
||||||
|
|
||||||
|
text = message.text or ""
|
||||||
|
|
||||||
|
if waiting_for_zip_password:
|
||||||
|
password = text.strip()
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
await message.reply_text("رمز نمیتواند خالی باشد. لطفاً یک رمز معتبر ارسال کنید.")
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = load_settings()
|
||||||
|
settings["safe_mode"] = True
|
||||||
|
settings["zip_password"] = password
|
||||||
|
save_settings(settings)
|
||||||
|
|
||||||
|
waiting_for_zip_password = False
|
||||||
|
|
||||||
|
await message.reply_text(
|
||||||
|
"رمز ذخیره شد.\n\n"
|
||||||
|
"از این به بعد فایلها قبل از ارسال به روبیکا بهصورت ZIP رمزدار آماده میشوند."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
url = extract_first_url(text)
|
||||||
|
|
||||||
|
if not url or not is_direct_url(url):
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = load_settings()
|
||||||
|
|
||||||
|
status = await message.reply_text(
|
||||||
|
"لینک دریافت شد.\n\n"
|
||||||
|
"وضعیت: در صف دانلود قرار گرفت."
|
||||||
|
)
|
||||||
|
|
||||||
|
task = {
|
||||||
|
"type": "direct_url",
|
||||||
|
"url": url,
|
||||||
|
"chat_id": message.chat.id,
|
||||||
|
"status_message_id": status.id,
|
||||||
|
"safe_mode": settings.get("safe_mode", False),
|
||||||
|
"zip_password": settings.get("zip_password", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.push(task)
|
||||||
|
|
||||||
|
await status.edit_text(
|
||||||
|
f"لینک در صف قرار گرفت.\n\n"
|
||||||
|
f"شناسه: `{task['job_id']}`\n"
|
||||||
|
f"برای حذف این مورد از صف:\n"
|
||||||
|
f"`/del {task['job_id']}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_message(
|
||||||
|
filters.private
|
||||||
|
& (
|
||||||
|
filters.document
|
||||||
|
| filters.video
|
||||||
|
| filters.audio
|
||||||
|
| filters.voice
|
||||||
|
| filters.photo
|
||||||
|
| filters.animation
|
||||||
|
| filters.video_note
|
||||||
|
| filters.sticker
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def media_handler(client: Client, message: Message):
|
||||||
|
media_type, media = get_media(message)
|
||||||
|
if not media:
|
||||||
|
await message.reply_text("فایل قابل پردازش نیست.")
|
||||||
|
return
|
||||||
|
|
||||||
|
download_name = build_download_filename(message, media_type, media)
|
||||||
|
download_path = DOWNLOAD_DIR / download_name
|
||||||
|
|
||||||
|
status = await message.reply_text(
|
||||||
|
"فایل دریافت شد.\n\n"
|
||||||
|
"وضعیت: آمادهسازی برای دانلود از تلگرام..."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
started_at = time.time()
|
||||||
|
progress_state = {"last_update": 0}
|
||||||
|
|
||||||
|
downloaded = await client.download_media(
|
||||||
|
message,
|
||||||
|
file_name=str(download_path),
|
||||||
|
progress=download_progress,
|
||||||
|
progress_args=(status, download_name, started_at, progress_state),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not downloaded:
|
||||||
|
raise RuntimeError("Download failed.")
|
||||||
|
|
||||||
|
downloaded_path = Path(downloaded)
|
||||||
|
if not downloaded_path.exists():
|
||||||
|
raise RuntimeError("Downloaded file not found.")
|
||||||
|
|
||||||
|
file_size = downloaded_path.stat().st_size
|
||||||
|
settings = load_settings()
|
||||||
|
|
||||||
|
task = {
|
||||||
|
"type": "local_file",
|
||||||
|
"path": str(downloaded_path),
|
||||||
|
"caption": message.caption or "",
|
||||||
|
"chat_id": message.chat.id,
|
||||||
|
"status_message_id": status.id,
|
||||||
|
"file_name": download_name,
|
||||||
|
"file_size": file_size,
|
||||||
|
"safe_mode": settings.get("safe_mode", False),
|
||||||
|
"zip_password": settings.get("zip_password", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.push(task)
|
||||||
|
|
||||||
|
await status.edit_text(
|
||||||
|
f"در صف قرار گرفت.\n\n"
|
||||||
|
f"فایل: `{download_name}`\n"
|
||||||
|
f"حجم: `{pretty_size(file_size)}`\n"
|
||||||
|
f"شناسه: `{task['job_id']}`\n\n"
|
||||||
|
f"برای حذف این مورد از صف:\n"
|
||||||
|
f"`/del {task['job_id']}`"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await status.edit_text(f"خطا: {str(e)}")
|
||||||
|
|
||||||
|
def clear_old_status():
|
||||||
|
try:
|
||||||
|
if STATUS_FILE.exists():
|
||||||
|
STATUS_FILE.unlink()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
clear_old_status()
|
||||||
|
app.start()
|
||||||
|
app.loop.create_task(status_watcher())
|
||||||
|
idle()
|
||||||
|
app.stop()
|
||||||
Reference in New Issue
Block a user