├── devgagan ├── core │ ├── __init__.py │ ├── mongo │ │ ├── __init__.py │ │ ├── users_db.py │ │ ├── plans_db.py │ │ └── db.py │ ├── func.py │ └── get_func.py ├── modules │ ├── __init__.py │ ├── stats.py │ ├── gcast.py │ ├── speedtest.py │ ├── login.py │ ├── shrink.py │ ├── eval.py │ ├── start.py │ ├── plans.py │ ├── main.py │ └── ytdl.py ├── __main__.py └── __init__.py ├── Procfile ├── settings.jpg ├── heroku.yml ├── app.py ├── requirements.txt ├── Dockerfile ├── config.py ├── LICENSE ├── TERMS_OF_USE.md ├── app.json ├── templates └── welcome.html └── README.md /devgagan/core/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python -m devgagan 2 | -------------------------------------------------------------------------------- /settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devgaganin/Save-Restricted-Content-Bot-v2/HEAD/settings.jpg -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | worker: Dockerfile 4 | run: 5 | worker: python3 -m devgagan 6 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, render_template 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route("/") 7 | def welcome(): 8 | # Render the welcome page with animated "Team SPY" text 9 | return render_template("welcome.html") 10 | 11 | if __name__ == "__main__": 12 | # Default to port 5000 if PORT is not set in the environment 13 | port = int(os.environ.get("PORT", 8000)) 14 | app.run(host="0.0.0.0", port=port) 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # https://www.dl.dropboxusercontent.com/scl/fi/35w0i0hnsioq63x7eumdd/pyrogagan.zip?rlkey=mq4b56v0z7w7mgj3c0o3sqgo1&st=n7prfm1y&dl=0 2 | pyrofork 3 | psutil 4 | aiofiles 5 | pillow 6 | devgagantools 7 | tgcrypto 8 | pyromod 9 | opencv-python-headless 10 | requests 11 | motor 12 | pytz 13 | flask 14 | aiohttp 15 | telethon 16 | aiojobs 17 | werkzeug==2.2.2 18 | apscheduler 19 | telethon-tgcrypto 20 | mutagen 21 | yt-dlp 22 | speedtest-cli 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.4-slim 2 | RUN apt update && apt upgrade -y 3 | RUN apt-get install git curl python3-pip ffmpeg -y 4 | RUN apt-get -y install git 5 | RUN apt-get install -y wget python3-pip curl bash neofetch ffmpeg software-properties-common 6 | COPY requirements.txt . 7 | 8 | RUN pip3 install wheel 9 | RUN pip3 install --no-cache-dir -U -r requirements.txt 10 | WORKDIR /app 11 | COPY . . 12 | EXPOSE 8000 13 | 14 | CMD flask run -h 0.0.0.0 -p 8000 & python3 -m devgagan 15 | -------------------------------------------------------------------------------- /devgagan/core/mongo/__init__.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: __init__.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | -------------------------------------------------------------------------------- /devgagan/modules/__init__.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: __init__.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | 16 | import glob 17 | from os.path import basename, dirname, isfile 18 | 19 | 20 | def __list_all_modules(): 21 | mod_paths = glob.glob(dirname(__file__) + "/*.py") 22 | 23 | all_modules = [ 24 | basename(f)[:-3] 25 | for f in mod_paths 26 | if isfile(f) and f.endswith(".py") and not f.endswith("__init__.py") 27 | ] 28 | 29 | return all_modules 30 | 31 | 32 | ALL_MODULES = sorted(__list_all_modules()) 33 | __all__ = ALL_MODULES + ["ALL_MODULES"] 34 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # devgagan 2 | # Note if you are trying to deploy on vps then directly fill values in ("") 3 | 4 | from os import getenv 5 | 6 | # VPS --- FILL COOKIES 🍪 in """ ... """ 7 | 8 | INST_COOKIES = """ 9 | # wtite up here insta cookies 10 | """ 11 | 12 | YTUB_COOKIES = """ 13 | # write here yt cookies 14 | """ 15 | 16 | API_ID = int(getenv("API_ID", "")) 17 | API_HASH = getenv("API_HASH", "") 18 | BOT_TOKEN = getenv("BOT_TOKEN", "") 19 | OWNER_ID = list(map(int, getenv("OWNER_ID", "").split())) 20 | MONGO_DB = getenv("MONGO_DB", "") 21 | LOG_GROUP = getenv("LOG_GROUP", "") 22 | CHANNEL_ID = int(getenv("CHANNEL_ID", "")) 23 | FREEMIUM_LIMIT = int(getenv("FREEMIUM_LIMIT", "0")) 24 | PREMIUM_LIMIT = int(getenv("PREMIUM_LIMIT", "500")) 25 | WEBSITE_URL = getenv("WEBSITE_URL", "upshrink.com") 26 | AD_API = getenv("AD_API", "52b4a2cf4687d81e7d3f8f2b7bc2943f618e78cb") 27 | STRING = getenv("STRING", None) 28 | YT_COOKIES = getenv("YT_COOKIES", YTUB_COOKIES) 29 | DEFAULT_SESSION = getenv("DEFAUL_SESSION", None) # added old method of invite link joining 30 | INSTA_COOKIES = getenv("INSTA_COOKIES", INST_COOKIES) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 devgaganin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /devgagan/core/mongo/users_db.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: users_db.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | from config import MONGO_DB 16 | from motor.motor_asyncio import AsyncIOMotorClient as MongoCli 17 | 18 | 19 | mongo = MongoCli(MONGO_DB) 20 | db = mongo.users 21 | db = db.users_db 22 | 23 | 24 | async def get_users(): 25 | user_list = [] 26 | async for user in db.users.find({"user": {"$gt": 0}}): 27 | user_list.append(user['user']) 28 | return user_list 29 | 30 | 31 | async def get_user(user): 32 | users = await get_users() 33 | if user in users: 34 | return True 35 | else: 36 | return False 37 | 38 | async def add_user(user): 39 | users = await get_users() 40 | if user in users: 41 | return 42 | else: 43 | await db.users.insert_one({"user": user}) 44 | 45 | 46 | async def del_user(user): 47 | users = await get_users() 48 | if not user in users: 49 | return 50 | else: 51 | await db.users.delete_one({"user": user}) 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /TERMS_OF_USE.md: -------------------------------------------------------------------------------- 1 | # Terms of Use 2 | 3 | By using this software, you agree to the following terms: 4 | 5 | 1. **License**: This software is provided under the terms of the MIT License. You are free to use, modify, and distribute this software, subject to the conditions of the license. A copy of the license is included in the LICENSE file. 6 | 7 | 2. **Attribution**: If you modify or build upon this software, you must provide appropriate credit by acknowledging the original author(s) and including a reference to the original project repository or source code. 8 | 9 | 3. **No Warranty**: This software is provided "as is," without any warranty, express or implied. The author(s) of this software are not liable for any damages or losses arising from the use or inability to use this software. 10 | 11 | 4. **Modification**: You are permitted to modify the software for your own use or for contributing improvements back to the community. However, you must adhere to the terms of the license and provide appropriate credit as outlined in point 2. 12 | 13 | 5. **Contribution**: Contributions to the project are welcome. By contributing code, documentation, or other materials, you agree to license your contributions under the terms of the MIT License. 14 | 15 | 6. **Compliance**: By using this software, you agree to comply with all applicable laws and regulations. Any use of this software for illegal or malicious purposes is strictly prohibited. 16 | 17 | 7. **Feedback**: Your feedback and suggestions for improvements to this software are appreciated. Please feel free to submit issues or pull requests to the project repository. 18 | 19 | By using this software, you acknowledge that you have read, understood, and agree to be bound by these terms. 20 | -------------------------------------------------------------------------------- /devgagan/core/mongo/plans_db.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: plans_db.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | import datetime 16 | from motor.motor_asyncio import AsyncIOMotorClient as MongoCli 17 | from config import MONGO_DB 18 | 19 | mongo = MongoCli(MONGO_DB) 20 | db = mongo.premium 21 | db = db.premium_db 22 | 23 | async def add_premium(user_id, expire_date): 24 | data = await check_premium(user_id) 25 | if data and data.get("_id"): 26 | await db.update_one({"_id": user_id}, {"$set": {"expire_date": expire_date}}) 27 | else: 28 | await db.insert_one({"_id": user_id, "expire_date": expire_date}) 29 | 30 | async def remove_premium(user_id): 31 | await db.delete_one({"_id": user_id}) 32 | 33 | async def check_premium(user_id): 34 | return await db.find_one({"_id": user_id}) 35 | 36 | async def premium_users(): 37 | id_list = [] 38 | async for data in db.find(): 39 | id_list.append(data["_id"]) 40 | return id_list 41 | 42 | async def check_and_remove_expired_users(): 43 | current_time = datetime.datetime.utcnow() 44 | async for data in db.find(): 45 | expire_date = data.get("expire_date") 46 | if expire_date and expire_date < current_time: 47 | await remove_premium(data["_id"]) 48 | print(f"Removed user {data['_id']} due to expired plan.") 49 | -------------------------------------------------------------------------------- /devgagan/__main__.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: __main__.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | import asyncio 16 | import importlib 17 | import gc 18 | from pyrogram import idle 19 | from devgagan.modules import ALL_MODULES 20 | from devgagan.core.mongo.plans_db import check_and_remove_expired_users 21 | from aiojobs import create_scheduler 22 | 23 | # ----------------------------Bot-Start---------------------------- # 24 | 25 | loop = asyncio.get_event_loop() 26 | 27 | # Function to schedule expiry checks 28 | async def schedule_expiry_check(): 29 | scheduler = await create_scheduler() 30 | while True: 31 | await scheduler.spawn(check_and_remove_expired_users()) 32 | await asyncio.sleep(60) # Check every hour 33 | gc.collect() 34 | 35 | async def devggn_boot(): 36 | for all_module in ALL_MODULES: 37 | importlib.import_module("devgagan.modules." + all_module) 38 | print(""" 39 | --------------------------------------------------- 40 | 📂 Bot Deployed successfully ... 41 | 📝 Description: A Pyrogram bot for downloading files from Telegram channels or groups 42 | and uploading them back to Telegram. 43 | 👨‍💻 Author: Gagan 44 | 🌐 GitHub: https://github.com/devgaganin/ 45 | 📬 Telegram: https://t.me/team_spy_pro 46 | ▶️ YouTube: https://youtube.com/@dev_gagan 47 | 🗓️ Created: 2025-01-11 48 | 🔄 Last Modified: 2025-01-11 49 | 🛠️ Version: 2.0.5 50 | 📜 License: MIT License 51 | --------------------------------------------------- 52 | """) 53 | 54 | asyncio.create_task(schedule_expiry_check()) 55 | print("Auto removal started ...") 56 | await idle() 57 | print("Bot stopped...") 58 | 59 | 60 | if __name__ == "__main__": 61 | loop.run_until_complete(devggn_boot()) 62 | 63 | # ------------------------------------------------------------------ # 64 | -------------------------------------------------------------------------------- /devgagan/modules/stats.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: stats.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | 16 | 17 | import time 18 | import sys 19 | import motor 20 | from devgagan import app 21 | from pyrogram import filters 22 | from config import OWNER_ID 23 | from devgagan.core.mongo.users_db import get_users, add_user, get_user 24 | from devgagan.core.mongo.plans_db import premium_users 25 | 26 | 27 | 28 | start_time = time.time() 29 | 30 | @app.on_message(group=10) 31 | async def chat_watcher_func(_, message): 32 | try: 33 | if message.from_user: 34 | us_in_db = await get_user(message.from_user.id) 35 | if not us_in_db: 36 | await add_user(message.from_user.id) 37 | except: 38 | pass 39 | 40 | 41 | 42 | def time_formatter(): 43 | minutes, seconds = divmod(int(time.time() - start_time), 60) 44 | hours, minutes = divmod(minutes, 60) 45 | days, hours = divmod(hours, 24) 46 | weeks, days = divmod(days, 7) 47 | tmp = ( 48 | ((str(weeks) + "w:") if weeks else "") 49 | + ((str(days) + "d:") if days else "") 50 | + ((str(hours) + "h:") if hours else "") 51 | + ((str(minutes) + "m:") if minutes else "") 52 | + ((str(seconds) + "s") if seconds else "") 53 | ) 54 | if tmp != "": 55 | if tmp.endswith(":"): 56 | return tmp[:-1] 57 | else: 58 | return tmp 59 | else: 60 | return "0 s" 61 | 62 | 63 | @app.on_message(filters.command("stats") & filters.user(OWNER_ID)) 64 | async def stats(client, message): 65 | start = time.time() 66 | users = len(await get_users()) 67 | premium = await premium_users() 68 | ping = round((time.time() - start) * 1000) 69 | await message.reply_text(f""" 70 | **Stats of** {(await client.get_me()).mention} : 71 | 72 | 🏓 **Ping Pong**: {ping}ms 73 | 74 | 📊 **Total Users** : `{users}` 75 | 📈 **Premium Users** : `{len(premium)}` 76 | ⚙️ **Bot Uptime** : `{time_formatter()}` 77 | 78 | 🎨 **Python Version**: `{sys.version.split()[0]}` 79 | 📑 **Mongo Version**: `{motor.version}` 80 | """) 81 | 82 | -------------------------------------------------------------------------------- /devgagan/__init__.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: __init__.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | import asyncio 16 | import logging 17 | import time 18 | from pyrogram import Client 19 | from pyrogram.enums import ParseMode 20 | from config import API_ID, API_HASH, BOT_TOKEN, STRING, MONGO_DB, DEFAULT_SESSION 21 | from telethon.sync import TelegramClient 22 | from motor.motor_asyncio import AsyncIOMotorClient 23 | 24 | loop = asyncio.new_event_loop() 25 | asyncio.set_event_loop(loop) 26 | 27 | logging.basicConfig( 28 | format="[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s", 29 | level=logging.INFO, 30 | ) 31 | 32 | botStartTime = time.time() 33 | 34 | app = Client( 35 | "pyrobot", 36 | api_id=API_ID, 37 | api_hash=API_HASH, 38 | bot_token=BOT_TOKEN, 39 | workers=50, 40 | parse_mode=ParseMode.MARKDOWN 41 | ) 42 | 43 | sex = TelegramClient('sexrepo', API_ID, API_HASH).start(bot_token=BOT_TOKEN) 44 | 45 | if STRING: 46 | pro = Client("ggbot", api_id=API_ID, api_hash=API_HASH, session_string=STRING) 47 | else: 48 | pro = None 49 | 50 | 51 | if DEFAULT_SESSION: 52 | userrbot = Client("userrbot", api_id=API_ID, api_hash=API_HASH, session_string=DEFAULT_SESSION) 53 | else: 54 | userrbot = None 55 | 56 | telethon_client = TelegramClient('telethon_session', API_ID, API_HASH).start(bot_token=BOT_TOKEN) 57 | 58 | # MongoDB setup 59 | tclient = AsyncIOMotorClient(MONGO_DB) 60 | tdb = tclient["telegram_bot"] # Your database 61 | token = tdb["tokens"] # Your tokens collection 62 | 63 | async def create_ttl_index(): 64 | """Ensure the TTL index exists for the `tokens` collection.""" 65 | await token.create_index("expires_at", expireAfterSeconds=0) 66 | 67 | # Run the TTL index creation when the bot starts 68 | async def setup_database(): 69 | await create_ttl_index() 70 | print("MongoDB TTL index created.") 71 | 72 | async def restrict_bot(): 73 | global BOT_ID, BOT_NAME, BOT_USERNAME 74 | await setup_database() 75 | await app.start() 76 | getme = await app.get_me() 77 | BOT_ID = getme.id 78 | BOT_USERNAME = getme.username 79 | BOT_NAME = f"{getme.first_name} {getme.last_name}" if getme.last_name else getme.first_name 80 | 81 | if pro: 82 | await pro.start() 83 | if userrbot: 84 | await userrbot.start() 85 | 86 | loop.run_until_complete(restrict_bot()) 87 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Save Restricted Content Bot master-v1 branch", 3 | "description": "Save Restricted Content Bot by Team SPY", 4 | "logo": "https://lh3.googleusercontent.com/-HPcn7AqepNg/AAAAAAAAAAI/AAAAAAAAAAA/ALKGfknb1BkQiq-8_KUVOYcNAJ4swKivDQ/photo.jpg", 5 | "keywords": ["python3", "telegram", "MusicBot", "telegram-bot", "pyrogram"], 6 | "repository": "https://github.com/devgaganin/save_restricted-content-telegram-bot-repo", 7 | "success_url": "https://devgagan.in", 8 | "env": { 9 | "API_ID": { 10 | "description": "Get this value from https://my.telegram.org", 11 | "value": "your_api_id_here", 12 | "required": true 13 | }, 14 | "API_HASH": { 15 | "description": "Get this value from https://my.telegram.org", 16 | "value": "your_api_hash_here", 17 | "required": true 18 | }, 19 | "BOT_TOKEN": { 20 | "description": "A Bot's token from Botfather", 21 | "value": "your_bot_token_here", 22 | "required": true 23 | }, 24 | "MONGO_DB": { 25 | "description": "Get a MongoDB URL from https://cloud.mongodb.com.", 26 | "value": "your_mongodb_url_here", 27 | "required": true 28 | }, 29 | "OWNER_ID": { 30 | "description": "The user ID of the user whom you would like to add as OWNER.", 31 | "value": "owner_user_id_here", 32 | "required": true 33 | }, 34 | "FREEMIUM_LIMIT": { 35 | "description": "Batch size for free users", 36 | "value": "0", 37 | "required": true 38 | }, 39 | "PREMIUM_LIMIT": { 40 | "description": "Batch size for premium users", 41 | "value": "500", 42 | "required": true 43 | }, 44 | "STRING": { 45 | "description": "Enter premium account session string if you want to allow the paid users to upload upto 4GB", 46 | "value": "", 47 | "required": false 48 | }, 49 | 50 | "DEFAULT_SESSION": { 51 | "description": "Enter session string if you want YOUR USERS not to be forced login and can do things via invite_link", 52 | "value": "", 53 | "required": false 54 | }, 55 | 56 | "WEBSITE_URL": { 57 | "description": "Enter shortener website domain eg upshrink.com", 58 | "value": "upshrink.com", 59 | "required": false 60 | }, 61 | "AD_API": { 62 | "description": "Batch size for premium users", 63 | "value": "52b4a2cf4687d81e7d3f8f2b7bc2943f618e78cb", 64 | "required": false 65 | }, 66 | 67 | "SUDO_USERS": { 68 | "description": "Other Admins' IDs (optional)", 69 | "value": "", 70 | "required": false 71 | }, 72 | "CHANNEL_ID": { 73 | "description": "Enter Channel ID (-100) and make bot admin there", 74 | "value": "channel_id_here", 75 | "required": true 76 | }, 77 | "LOG_GROUP": { 78 | "description": "Enter Log Channel/Group ID (-100) and make bot admin there", 79 | "value": "log_group_id_here", 80 | "required": true 81 | } 82 | }, 83 | "buildpacks": [ 84 | { "url": "heroku/python" }, 85 | { "url": "https://github.com/heroku/heroku-buildpack-activestorage-preview" } 86 | ], 87 | "stack": "container" 88 | } 89 | -------------------------------------------------------------------------------- /devgagan/modules/gcast.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: gcast.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | import asyncio 16 | from pyrogram import filters 17 | from config import OWNER_ID 18 | from devgagan import app 19 | from devgagan.core.mongo.users_db import get_users 20 | 21 | async def send_msg(user_id, message): 22 | try: 23 | x = await message.copy(chat_id=user_id) 24 | try: 25 | await x.pin() 26 | except Exception: 27 | await x.pin(both_sides=True) 28 | except FloodWait as e: 29 | await asyncio.sleep(e.x) 30 | return send_msg(user_id, message) 31 | except InputUserDeactivated: 32 | return 400, f"{user_id} : deactivated\n" 33 | except UserIsBlocked: 34 | return 400, f"{user_id} : blocked the bot\n" 35 | except PeerIdInvalid: 36 | return 400, f"{user_id} : user id invalid\n" 37 | except Exception: 38 | return 500, f"{user_id} : {traceback.format_exc()}\n" 39 | 40 | 41 | @app.on_message(filters.command("gcast") & filters.user(OWNER_ID)) 42 | async def broadcast(_, message): 43 | if not message.reply_to_message: 44 | await message.reply_text("ʀᴇᴘʟʏ ᴛᴏ ᴀ ᴍᴇssᴀɢᴇ ᴛᴏ ʙʀᴏᴀᴅᴄᴀsᴛ ɪᴛ.") 45 | return 46 | exmsg = await message.reply_text("sᴛᴀʀᴛᴇᴅ ʙʀᴏᴀᴅᴄᴀsᴛɪɴɢ!") 47 | all_users = (await get_users()) or {} 48 | done_users = 0 49 | failed_users = 0 50 | 51 | for user in all_users: 52 | try: 53 | await send_msg(user, message.reply_to_message) 54 | done_users += 1 55 | await asyncio.sleep(0.1) 56 | except Exception: 57 | pass 58 | failed_users += 1 59 | if failed_users == 0: 60 | await exmsg.edit_text( 61 | f"**sᴜᴄᴄᴇssғᴜʟʟʏ ʙʀᴏᴀᴅᴄᴀsᴛɪɴɢ ✅**\n\n**sᴇɴᴛ ᴍᴇssᴀɢᴇ ᴛᴏ** `{done_users}` **ᴜsᴇʀs**", 62 | ) 63 | else: 64 | await exmsg.edit_text( 65 | f"**sᴜᴄᴄᴇssғᴜʟʟʏ ʙʀᴏᴀᴅᴄᴀsᴛɪɴɢ ✅**\n\n**sᴇɴᴛ ᴍᴇssᴀɢᴇ ᴛᴏ** `{done_users}` **ᴜsᴇʀs**\n\n**ɴᴏᴛᴇ:-** `ᴅᴜᴇ ᴛᴏ sᴏᴍᴇ ɪssᴜᴇ ᴄᴀɴ'ᴛ ᴀʙʟᴇ ᴛᴏ ʙʀᴏᴀᴅᴄᴀsᴛ` `{failed_users}` **ᴜsᴇʀs**", 66 | ) 67 | 68 | 69 | 70 | 71 | 72 | @app.on_message(filters.command("acast") & filters.user(OWNER_ID)) 73 | async def announced(_, message): 74 | if message.reply_to_message: 75 | to_send=message.reply_to_message.id 76 | if not message.reply_to_message: 77 | return await message.reply_text("Reply To Some Post To Broadcast") 78 | users = await get_users() or [] 79 | print(users) 80 | failed_user = 0 81 | 82 | for user in users: 83 | try: 84 | await _.forward_messages(chat_id=int(user), from_chat_id=message.chat.id, message_ids=to_send) 85 | await asyncio.sleep(1) 86 | except Exception as e: 87 | failed_user += 1 88 | 89 | if failed_users == 0: 90 | await exmsg.edit_text( 91 | f"**sᴜᴄᴄᴇssғᴜʟʟʏ ʙʀᴏᴀᴅᴄᴀsᴛɪɴɢ ✅**\n\n**sᴇɴᴛ ᴍᴇssᴀɢᴇ ᴛᴏ** `{done_users}` **ᴜsᴇʀs**", 92 | ) 93 | else: 94 | await exmsg.edit_text( 95 | f"**sᴜᴄᴄᴇssғᴜʟʟʏ ʙʀᴏᴀᴅᴄᴀsᴛɪɴɢ ✅**\n\n**sᴇɴᴛ ᴍᴇssᴀɢᴇ ᴛᴏ** `{done_users}` **ᴜsᴇʀs**\n\n**ɴᴏᴛᴇ:-** `ᴅᴜᴇ ᴛᴏ sᴏᴍᴇ ɪssᴜᴇ ᴄᴀɴ'ᴛ ᴀʙʟᴇ ᴛᴏ ʙʀᴏᴀᴅᴄᴀsᴛ` `{failed_users}` **ᴜsᴇʀs**", 96 | ) 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /devgagan/core/mongo/db.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: db.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | from config import MONGO_DB 16 | from motor.motor_asyncio import AsyncIOMotorClient as MongoCli 17 | mongo = MongoCli(MONGO_DB) 18 | db = mongo.user_data 19 | db = db.users_data_db 20 | async def get_data(user_id): 21 | x = await db.find_one({"_id": user_id}) 22 | return x 23 | async def set_thumbnail(user_id, thumb): 24 | data = await get_data(user_id) 25 | if data and data.get("_id"): 26 | await db.update_one({"_id": user_id}, {"$set": {"thumb": thumb}}) 27 | else: 28 | await db.insert_one({"_id": user_id, "thumb": thumb}) 29 | async def set_caption(user_id, caption): 30 | data = await get_data(user_id) 31 | if data and data.get("_id"): 32 | await db.update_one({"_id": user_id}, {"$set": {"caption": caption}}) 33 | else: 34 | await db.insert_one({"_id": user_id, "caption": caption}) 35 | async def replace_caption(user_id, replace_txt, to_replace): 36 | data = await get_data(user_id) 37 | if data and data.get("_id"): 38 | await db.update_one({"_id": user_id}, {"$set": {"replace_txt": replace_txt, "to_replace": to_replace}}) 39 | else: 40 | await db.insert_one({"_id": user_id, "replace_txt": replace_txt, "to_replace": to_replace}) 41 | async def set_session(user_id, session): 42 | data = await get_data(user_id) 43 | if data and data.get("_id"): 44 | await db.update_one({"_id": user_id}, {"$set": {"session": session}}) 45 | else: 46 | await db.insert_one({"_id": user_id, "session": session}) 47 | async def clean_words(user_id, new_clean_words): 48 | data = await get_data(user_id) 49 | if data and data.get("_id"): 50 | existing_words = data.get("clean_words", []) 51 | 52 | if existing_words is None: 53 | existing_words = [] 54 | updated_words = list(set(existing_words + new_clean_words)) 55 | await db.update_one({"_id": user_id}, {"$set": {"clean_words": updated_words}}) 56 | else: 57 | await db.insert_one({"_id": user_id, "clean_words": new_clean_words}) 58 | async def remove_clean_words(user_id, words_to_remove): 59 | data = await get_data(user_id) 60 | if data and data.get("_id"): 61 | existing_words = data.get("clean_words", []) 62 | updated_words = [word for word in existing_words if word not in words_to_remove] 63 | await db.update_one({"_id": user_id}, {"$set": {"clean_words": updated_words}}) 64 | else: 65 | await db.insert_one({"_id": user_id, "clean_words": []}) 66 | async def set_channel(user_id, chat_id): 67 | data = await get_data(user_id) 68 | if data and data.get("_id"): 69 | await db.update_one({"_id": user_id}, {"$set": {"chat_id": chat_id}}) 70 | else: 71 | await db.insert_one({"_id": user_id, "chat_id": chat_id}) 72 | async def all_words_remove(user_id): 73 | await db.update_one({"_id": user_id}, {"$set": {"clean_words": None}}) 74 | async def remove_thumbnail(user_id): 75 | await db.update_one({"_id": user_id}, {"$set": {"thumb": None}}) 76 | async def remove_caption(user_id): 77 | await db.update_one({"_id": user_id}, {"$set": {"caption": None}}) 78 | async def remove_replace(user_id): 79 | await db.update_one({"_id": user_id}, {"$set": {"replace_txt": None, "to_replace": None}}) 80 | 81 | async def remove_session(user_id): 82 | await db.update_one({"_id": user_id}, {"$set": {"session": None}}) 83 | async def remove_channel(user_id): 84 | await db.update_one({"_id": user_id}, {"$set": {"chat_id": None}}) 85 | async def delete_session(user_id): 86 | """Delete the session associated with the given user_id from the database.""" 87 | await db.update_one({"_id": user_id}, {"$unset": {"session": ""}}) 88 | -------------------------------------------------------------------------------- /devgagan/modules/speedtest.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: speedtest.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | from time import time 16 | from speedtest import Speedtest 17 | import math 18 | from telethon import events 19 | from devgagan import botStartTime 20 | from devgagan import sex as gagan 21 | 22 | SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] 23 | 24 | def get_readable_time(seconds: int) -> str: 25 | result = '' 26 | (days, remainder) = divmod(seconds, 86400) 27 | days = int(days) 28 | if days != 0: 29 | result += f'{days}d' 30 | (hours, remainder) = divmod(remainder, 3600) 31 | hours = int(hours) 32 | if hours != 0: 33 | result += f'{hours}h' 34 | (minutes, seconds) = divmod(remainder, 60) 35 | minutes = int(minutes) 36 | if minutes != 0: 37 | result += f'{minutes}m' 38 | seconds = int(seconds) 39 | result += f'{seconds}s' 40 | return result 41 | 42 | def get_readable_file_size(size_in_bytes) -> str: 43 | if size_in_bytes is None: 44 | return '0B' 45 | index = 0 46 | while size_in_bytes >= 1024: 47 | size_in_bytes /= 1024 48 | index += 1 49 | try: 50 | return f'{round(size_in_bytes, 2)}{SIZE_UNITS[index]}' 51 | except IndexError: 52 | return 'File too large' 53 | 54 | 55 | @gagan.on(events.NewMessage(incoming=True, pattern='/speedtest')) 56 | async def speedtest(event): 57 | speed = await event.reply("Running Speed Test. Wait about some secs.") #edit telethon 58 | test = Speedtest() 59 | test.get_best_server() 60 | test.download() 61 | test.upload() 62 | test.results.share() 63 | result = test.results.dict() 64 | path = (result['share']) 65 | currentTime = get_readable_time(time() - botStartTime) 66 | string_speed = f''' 67 | ╭─《 🚀 SPEEDTEST INFO 》 68 | ├ Upload: {speed_convert(result['upload'], False)} 69 | ├ Download: {speed_convert(result['download'], False)} 70 | ├ Ping: {result['ping']} ms 71 | ├ Time: {result['timestamp']} 72 | ├ Data Sent: {get_readable_file_size(int(result['bytes_sent']))} 73 | ╰ Data Received: {get_readable_file_size(int(result['bytes_received']))} 74 | ╭─《 🌐 SPEEDTEST SERVER 》 75 | ├ Name: {result['server']['name']} 76 | ├ Country: {result['server']['country']}, {result['server']['cc']} 77 | ├ Sponsor: {result['server']['sponsor']} 78 | ├ Latency: {result['server']['latency']} 79 | ├ Latitude: {result['server']['lat']} 80 | ╰ Longitude: {result['server']['lon']} 81 | ╭─《 👤 CLIENT DETAILS 》 82 | ├ IP Address: {result['client']['ip']} 83 | ├ Latitude: {result['client']['lat']} 84 | ├ Longitude: {result['client']['lon']} 85 | ├ Country: {result['client']['country']} 86 | ├ ISP: {result['client']['isp']} 87 | ├ ISP Rating: {result['client']['isprating']} 88 | ╰ Powered by Team SPY 89 | ''' 90 | try: 91 | await event.reply(string_speed,file=path,parse_mode='html') 92 | await speed.delete() 93 | except Exception as g: 94 | await speed.delete() 95 | await event.reply(string_speed,parse_mode='html' ) 96 | 97 | def speed_convert(size, byte=True): 98 | if not byte: size = size / 8 99 | power = 2 ** 10 100 | zero = 0 101 | units = {0: "B/s", 1: "KB/s", 2: "MB/s", 3: "GB/s", 4: "TB/s"} 102 | while size > power: 103 | size /= power 104 | zero += 1 105 | return f"{round(size, 2)} {units[zero]}" 106 | -------------------------------------------------------------------------------- /templates/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Team SPY - Bot is Live 7 | 123 | 124 | 125 | 126 |
127 |
Team SPY
128 |
⚡ Bot is Live ⚡
129 | 130 |
131 | Join Us 132 | GitHub 133 |
134 |
135 | 136 | 137 | -------------------------------------------------------------------------------- /devgagan/modules/login.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: login.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | from pyrogram import filters, Client 16 | from devgagan import app 17 | import random 18 | import os 19 | import asyncio 20 | import string 21 | from devgagan.core.mongo import db 22 | from devgagan.core.func import subscribe, chk_user 23 | from config import API_ID as api_id, API_HASH as api_hash 24 | from pyrogram.errors import ( 25 | ApiIdInvalid, 26 | PhoneNumberInvalid, 27 | PhoneCodeInvalid, 28 | PhoneCodeExpired, 29 | SessionPasswordNeeded, 30 | PasswordHashInvalid, 31 | FloodWait 32 | ) 33 | 34 | def generate_random_name(length=7): 35 | characters = string.ascii_letters + string.digits 36 | return ''.join(random.choice(characters) for _ in range(length)) # Editted ... 37 | 38 | async def delete_session_files(user_id): 39 | session_file = f"session_{user_id}.session" 40 | memory_file = f"session_{user_id}.session-journal" 41 | 42 | session_file_exists = os.path.exists(session_file) 43 | memory_file_exists = os.path.exists(memory_file) 44 | 45 | if session_file_exists: 46 | os.remove(session_file) 47 | 48 | if memory_file_exists: 49 | os.remove(memory_file) 50 | 51 | # Delete session from the database 52 | if session_file_exists or memory_file_exists: 53 | await db.remove_session(user_id) 54 | return True # Files were deleted 55 | return False # No files found 56 | 57 | @app.on_message(filters.command("logout")) 58 | async def clear_db(client, message): 59 | user_id = message.chat.id 60 | files_deleted = await delete_session_files(user_id) 61 | try: 62 | await db.remove_session(user_id) 63 | except Exception: 64 | pass 65 | 66 | if files_deleted: 67 | await message.reply("✅ Your session data and files have been cleared from memory and disk.") 68 | else: 69 | await message.reply("✅ Logged out with flag -m") 70 | 71 | 72 | @app.on_message(filters.command("login")) 73 | async def generate_session(_, message): 74 | joined = await subscribe(_, message) 75 | if joined == 1: 76 | return 77 | 78 | # user_checked = await chk_user(message, message.from_user.id) 79 | # if user_checked == 1: 80 | # return 81 | 82 | user_id = message.chat.id 83 | 84 | number = await _.ask(user_id, 'Please enter your phone number along with the country code. \nExample: +19876543210', filters=filters.text) 85 | phone_number = number.text 86 | try: 87 | await message.reply("📲 Sending OTP...") 88 | client = Client(f"session_{user_id}", api_id, api_hash) 89 | 90 | await client.connect() 91 | except Exception as e: 92 | await message.reply(f"❌ Failed to send OTP {e}. Please wait and try again later.") 93 | try: 94 | code = await client.send_code(phone_number) 95 | except ApiIdInvalid: 96 | await message.reply('❌ Invalid combination of API ID and API HASH. Please restart the session.') 97 | return 98 | except PhoneNumberInvalid: 99 | await message.reply('❌ Invalid phone number. Please restart the session.') 100 | return 101 | try: 102 | otp_code = await _.ask(user_id, "Please check for an OTP in your official Telegram account. Once received, enter the OTP in the following format: \nIf the OTP is `12345`, please enter it as `1 2 3 4 5`.", filters=filters.text, timeout=600) 103 | except TimeoutError: 104 | await message.reply('⏰ Time limit of 10 minutes exceeded. Please restart the session.') 105 | return 106 | phone_code = otp_code.text.replace(" ", "") 107 | try: 108 | await client.sign_in(phone_number, code.phone_code_hash, phone_code) 109 | 110 | except PhoneCodeInvalid: 111 | await message.reply('❌ Invalid OTP. Please restart the session.') 112 | return 113 | except PhoneCodeExpired: 114 | await message.reply('❌ Expired OTP. Please restart the session.') 115 | return 116 | except SessionPasswordNeeded: 117 | try: 118 | two_step_msg = await _.ask(user_id, 'Your account has two-step verification enabled. Please enter your password.', filters=filters.text, timeout=300) 119 | except TimeoutError: 120 | await message.reply('⏰ Time limit of 5 minutes exceeded. Please restart the session.') 121 | return 122 | try: 123 | password = two_step_msg.text 124 | await client.check_password(password=password) 125 | except PasswordHashInvalid: 126 | await two_step_msg.reply('❌ Invalid password. Please restart the session.') 127 | return 128 | string_session = await client.export_session_string() 129 | await db.set_session(user_id, string_session) 130 | await client.disconnect() 131 | await otp_code.reply("✅ Login successful!") 132 | -------------------------------------------------------------------------------- /devgagan/modules/shrink.py: -------------------------------------------------------------------------------- 1 | 2 | # --------------------------------------------------- 3 | # File Name: shrink.py 4 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 5 | # and uploading them back to Telegram. 6 | # Author: Gagan 7 | # GitHub: https://github.com/devgaganin/ 8 | # Telegram: https://t.me/team_spy_pro 9 | # YouTube: https://youtube.com/@dev_gagan 10 | # Created: 2025-01-11 11 | # Last Modified: 2025-01-11 12 | # Version: 2.0.5 13 | # License: MIT License 14 | # --------------------------------------------------- 15 | 16 | from pyrogram import Client, filters 17 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 18 | import random 19 | import requests 20 | import string 21 | import aiohttp 22 | from devgagan import app 23 | from devgagan.core.func import * 24 | from datetime import datetime, timedelta 25 | from motor.motor_asyncio import AsyncIOMotorClient 26 | from config import MONGO_DB, WEBSITE_URL, AD_API, LOG_GROUP 27 | 28 | 29 | tclient = AsyncIOMotorClient(MONGO_DB) 30 | tdb = tclient["telegram_bot"] 31 | token = tdb["tokens"] 32 | 33 | 34 | async def create_ttl_index(): 35 | await token.create_index("expires_at", expireAfterSeconds=0) 36 | 37 | 38 | 39 | Param = {} 40 | 41 | 42 | async def generate_random_param(length=8): 43 | """Generate a random parameter.""" 44 | return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) 45 | 46 | 47 | async def get_shortened_url(deep_link): 48 | api_url = f"https://{WEBSITE_URL}/api?api={AD_API}&url={deep_link}" 49 | 50 | 51 | async with aiohttp.ClientSession() as session: 52 | async with session.get(api_url) as response: 53 | if response.status == 200: 54 | data = await response.json() 55 | if data.get("status") == "success": 56 | return data.get("shortenedUrl") 57 | return None 58 | 59 | 60 | async def is_user_verified(user_id): 61 | """Check if a user has an active session.""" 62 | session = await token.find_one({"user_id": user_id}) 63 | return session is not None 64 | 65 | 66 | @app.on_message(filters.command("start")) 67 | async def token_handler(client, message): 68 | """Handle the /token command.""" 69 | join = await subscribe(client, message) 70 | if join == 1: 71 | return 72 | chat_id = "save_restricted_content_bots" 73 | msg = await app.get_messages(chat_id, 796) 74 | user_id = message.chat.id 75 | if len(message.command) <= 1: 76 | image_url = "https://i.postimg.cc/v8q8kGyz/startimg-1.jpg" 77 | join_button = InlineKeyboardButton("Join Channel", url="https://t.me/team_spy_pro") 78 | premium = InlineKeyboardButton("Get Premium", url="https://t.me/kingofpatal") 79 | keyboard = InlineKeyboardMarkup([ 80 | [join_button], 81 | [premium] 82 | ]) 83 | 84 | await message.reply_photo( 85 | msg.photo.file_id, 86 | caption=( 87 | "Hi 👋 Welcome, Wanna intro...?\n\n" 88 | "✳️ I can save posts from channels or groups where forwarding is off. I can download videos/audio from YT, INSTA, ... social platforms\n" 89 | "✳️ Simply send the post link of a public channel. For private channels, do /login. Send /help to know more." 90 | ), 91 | reply_markup=keyboard 92 | ) 93 | return 94 | 95 | param = message.command[1] if len(message.command) > 1 else None 96 | freecheck = await chk_user(message, user_id) 97 | if freecheck != 1: 98 | await message.reply("You are a premium user no need of token 😉") 99 | return 100 | 101 | 102 | if param: 103 | if user_id in Param and Param[user_id] == param: 104 | 105 | await token.insert_one({ 106 | "user_id": user_id, 107 | "param": param, 108 | "created_at": datetime.utcnow(), 109 | "expires_at": datetime.utcnow() + timedelta(hours=3), 110 | }) 111 | del Param[user_id] 112 | await message.reply("✅ You have been verified successfully! Enjoy your session for next 3 hours.") 113 | return 114 | else: 115 | await message.reply("❌ Invalid or expired verification link. Please generate a new token.") 116 | return 117 | 118 | @app.on_message(filters.command("token")) 119 | async def smart_handler(client, message): 120 | user_id = message.chat.id 121 | 122 | freecheck = await chk_user(message, user_id) 123 | if freecheck != 1: 124 | await message.reply("You are a premium user no need of token 😉") 125 | return 126 | if await is_user_verified(user_id): 127 | await message.reply("✅ Your free session is already active enjoy!") 128 | else: 129 | 130 | param = await generate_random_param() 131 | Param[user_id] = param 132 | 133 | 134 | deep_link = f"https://t.me/{client.me.username}?start={param}" 135 | 136 | 137 | shortened_url = await get_shortened_url(deep_link) 138 | if not shortened_url: 139 | await message.reply("❌ Failed to generate the token link. Please try again.") 140 | return 141 | 142 | 143 | button = InlineKeyboardMarkup( 144 | [[InlineKeyboardButton("Verify the token now...", url=shortened_url)]] 145 | ) 146 | await message.reply("Click the button below to verify your free access token: \n\n> What will you get ? \n1. No time bound upto 3 hours \n2. Batch command limit will be FreeLimit + 20 \n3. All functions unlocked", reply_markup=button) 147 | -------------------------------------------------------------------------------- /devgagan/modules/eval.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: eval.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Unknown 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | import os, re, subprocess, sys, traceback 16 | from inspect import getfullargspec 17 | from io import StringIO 18 | from time import time 19 | from pyrogram import filters 20 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 21 | from config import OWNER_ID 22 | from devgagan import app 23 | 24 | async def aexec(code, client, message): 25 | exec( 26 | "async def __aexec(client, message): " 27 | + "".join(f"\n {a}" for a in code.split("\n")) 28 | ) 29 | return await locals()["__aexec"](client, message) 30 | 31 | 32 | async def edit_or_reply(msg, **kwargs): 33 | func = msg.edit_text if msg.from_user.is_self else msg.reply 34 | spec = getfullargspec(func.__wrapped__).args 35 | await func(**{k: v for k, v in kwargs.items() if k in spec}) 36 | 37 | 38 | @app.on_edited_message( 39 | filters.command(["evv", "evr"]) 40 | & filters.user(OWNER_ID) 41 | & ~filters.forwarded 42 | & ~filters.via_bot 43 | ) 44 | @app.on_message( 45 | filters.command(["evv", "evr"]) 46 | & filters.user(OWNER_ID) 47 | & ~filters.forwarded 48 | & ~filters.via_bot 49 | ) 50 | async def executor(client, message): 51 | if len(message.command) < 2: 52 | return await edit_or_reply(message, text="No command was given to execute!") 53 | try: 54 | cmd = message.text.split(" ", maxsplit=1)[1] 55 | except IndexError: 56 | return await message.delete() 57 | t1 = time() 58 | old_stderr = sys.stderr 59 | old_stdout = sys.stdout 60 | redirected_output = sys.stdout = StringIO() 61 | redirected_error = sys.stderr = StringIO() 62 | stdout, stderr, exc = None, None, None 63 | try: 64 | await aexec(cmd, client, message) 65 | except Exception: 66 | exc = traceback.format_exc() 67 | stdout = redirected_output.getvalue() 68 | stderr = redirected_error.getvalue() 69 | sys.stdout = old_stdout 70 | sys.stderr = old_stderr 71 | evaluation = "\n" 72 | if exc: 73 | evaluation += exc 74 | elif stderr: 75 | evaluation += stderr 76 | elif stdout: 77 | evaluation += stdout 78 | else: 79 | evaluation += "sᴜᴄᴄᴇss" 80 | final_output = f"📕 ʀᴇsᴜʟᴛ :\n
{evaluation}
" 81 | if len(final_output) > 4096: 82 | filename = "output.txt" 83 | with open(filename, "w+", encoding="utf8") as out_file: 84 | out_file.write(str(evaluation)) 85 | t2 = time() 86 | keyboard = InlineKeyboardMarkup( 87 | [ 88 | [ 89 | InlineKeyboardButton( 90 | text="⏳", 91 | callback_data=f"runtime {t2-t1} Seconds", 92 | ) 93 | ] 94 | ] 95 | ) 96 | await message.reply_document( 97 | document=filename, 98 | caption=f"🔗 ᴇᴠᴀʟ :\n{cmd[0:980]}\n\n📕 ʀᴇsᴜʟᴛ :\nᴀᴛᴛᴀᴄʜᴇᴅ ᴅᴏᴄᴜᴍᴇɴᴛ", 99 | quote=False, 100 | reply_markup=keyboard, 101 | ) 102 | await message.delete() 103 | os.remove(filename) 104 | else: 105 | t2 = time() 106 | keyboard = InlineKeyboardMarkup( 107 | [ 108 | [ 109 | InlineKeyboardButton( 110 | text="⏳", 111 | callback_data=f"runtime {round(t2-t1, 3)} Seconds", 112 | ), 113 | InlineKeyboardButton( 114 | text="🗑", 115 | callback_data=f"forceclose abc|{message.from_user.id}", 116 | ), 117 | ] 118 | ] 119 | ) 120 | await edit_or_reply(message, text=final_output, reply_markup=keyboard) 121 | 122 | 123 | @app.on_callback_query(filters.regex(r"runtime")) 124 | async def runtime_func_cq(_, cq): 125 | runtime = cq.data.split(None, 1)[1] 126 | await cq.answer(runtime, show_alert=True) 127 | 128 | 129 | @app.on_callback_query(filters.regex("fclose")) 130 | async def forceclose_command(_, CallbackQuery): 131 | callback_data = CallbackQuery.data.strip() 132 | callback_request = callback_data.split(None, 1)[1] 133 | query, user_id = callback_request.split("|") 134 | if CallbackQuery.from_user.id != int(user_id): 135 | try: 136 | return await CallbackQuery.answer( 137 | "ɪᴛ'ʟʟ ʙᴇ ʙᴇᴛᴛᴇʀ ɪғ ʏᴏᴜ sᴛᴀʏ ɪɴ ʏᴏᴜʀ ʟɪᴍɪᴛs ʙᴀʙʏ.", show_alert=True 138 | ) 139 | except: 140 | return 141 | await CallbackQuery.message.delete() 142 | try: 143 | await CallbackQuery.answer() 144 | except: 145 | return 146 | 147 | 148 | 149 | 150 | @app.on_edited_message( 151 | filters.command("shll") 152 | & filters.user(OWNER_ID) 153 | & ~filters.forwarded 154 | & ~filters.via_bot 155 | ) 156 | @app.on_message( 157 | filters.command("shll") 158 | & filters.user(OWNER_ID) 159 | & ~filters.forwarded 160 | & ~filters.via_bot 161 | ) 162 | async def shellrunner(_, message): 163 | if len(message.command) < 2: 164 | return await edit_or_reply(message, text="ᴇxᴀᴍᴩʟᴇ :\n/sh git pull") 165 | text = message.text.split(None, 1)[1] 166 | if "\n" in text: 167 | code = text.split("\n") 168 | output = "" 169 | for x in code: 170 | shell = re.split(""" (?=(?:[^'"]|'[^']*'|"[^"]*")*$)""", x) 171 | try: 172 | process = subprocess.Popen( 173 | shell, 174 | stdout=subprocess.PIPE, 175 | stderr=subprocess.PIPE, 176 | ) 177 | except Exception as err: 178 | await edit_or_reply(message, text=f"ERROR :\n
{err}
") 179 | output += f"{code}\n" 180 | output += process.stdout.read()[:-1].decode("utf-8") 181 | output += "\n" 182 | else: 183 | shell = re.split(""" (?=(?:[^'"]|'[^']*'|"[^"]*")*$)""", text) 184 | for a in range(len(shell)): 185 | shell[a] = shell[a].replace('"', "") 186 | try: 187 | process = subprocess.Popen( 188 | shell, 189 | stdout=subprocess.PIPE, 190 | stderr=subprocess.PIPE, 191 | ) 192 | except Exception as err: 193 | print(err) 194 | exc_type, exc_obj, exc_tb = sys.exc_info() 195 | errors = traceback.format_exception( 196 | etype=exc_type, 197 | value=exc_obj, 198 | tb=exc_tb, 199 | ) 200 | return await edit_or_reply( 201 | message, text=f"ERROR :\n
{''.join(errors)}
" 202 | ) 203 | output = process.stdout.read()[:-1].decode("utf-8") 204 | if str(output) == "\n": 205 | output = None 206 | if output: 207 | if len(output) > 4096: 208 | with open("output.txt", "w+") as file: 209 | file.write(output) 210 | await _.send_document( 211 | message.chat.id, 212 | "output.txt", 213 | reply_to_message_id=message.id, 214 | caption="Output", 215 | ) 216 | return os.remove("output.txt") 217 | await edit_or_reply(message, text=f"OUTPUT :\n
{output}
") 218 | else: 219 | await edit_or_reply(message, text="OUTPUT :\nNone") 220 | await message.stop_propagation() 221 | 222 | 223 | @app.on_message(filters.command("restart") & filters.user(OWNER_ID)) 224 | async def update(_, message): 225 | await message.reply("Restarting ... ") 226 | os.execl(sys.executable, sys.executable, "-m", "devgagan") 227 | -------------------------------------------------------------------------------- /devgagan/core/func.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: func.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | import math 16 | import time , re 17 | from pyrogram import enums 18 | from config import CHANNEL_ID, OWNER_ID 19 | from devgagan.core.mongo.plans_db import premium_users 20 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 21 | import cv2 22 | from pyrogram.errors import FloodWait, InviteHashInvalid, InviteHashExpired, UserAlreadyParticipant, UserNotParticipant 23 | from datetime import datetime as dt 24 | import asyncio, subprocess, re, os, time 25 | async def chk_user(message, user_id): 26 | user = await premium_users() 27 | if user_id in user or user_id in OWNER_ID: 28 | return 0 29 | else: 30 | return 1 31 | async def gen_link(app,chat_id): 32 | link = await app.export_chat_invite_link(chat_id) 33 | return link 34 | 35 | async def subscribe(app, message): 36 | update_channel = CHANNEL_ID 37 | url = await gen_link(app, update_channel) 38 | if update_channel: 39 | try: 40 | user = await app.get_chat_member(update_channel, message.from_user.id) 41 | if user.status == "kicked": 42 | await message.reply_text("You are Banned. Contact -- @devgaganin") 43 | return 1 44 | except UserNotParticipant: 45 | caption = f"Join our channel to use the bot" 46 | await message.reply_photo(photo="https://graph.org/file/d44f024a08ded19452152.jpg",caption=caption, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Join Now...", url=f"{url}")]])) 47 | return 1 48 | except Exception: 49 | await message.reply_text("Something Went Wrong. Contact us @devgaganin...") 50 | return 1 51 | async def get_seconds(time_string): 52 | def extract_value_and_unit(ts): 53 | value = "" 54 | unit = "" 55 | 56 | index = 0 57 | while index < len(ts) and ts[index].isdigit(): 58 | value += ts[index] 59 | index += 1 60 | 61 | unit = ts[index:].lstrip() 62 | 63 | if value: 64 | value = int(value) 65 | 66 | return value, unit 67 | 68 | value, unit = extract_value_and_unit(time_string) 69 | 70 | if unit == 's': 71 | return value 72 | elif unit == 'min': 73 | return value * 60 74 | elif unit == 'hour': 75 | return value * 3600 76 | elif unit == 'day': 77 | return value * 86400 78 | elif unit == 'month': 79 | return value * 86400 * 30 80 | elif unit == 'year': 81 | return value * 86400 * 365 82 | else: 83 | return 0 84 | PROGRESS_BAR = """\n 85 | │ **__Completed:__** {1}/{2} 86 | │ **__Bytes:__** {0}% 87 | │ **__Speed:__** {3}/s 88 | │ **__ETA:__** {4} 89 | ╰─────────────────────╯ 90 | """ 91 | async def progress_bar(current, total, ud_type, message, start): 92 | 93 | now = time.time() 94 | diff = now - start 95 | if round(diff % 10.00) == 0 or current == total: 96 | 97 | percentage = current * 100 / total 98 | speed = current / diff 99 | elapsed_time = round(diff) * 1000 100 | time_to_completion = round((total - current) / speed) * 1000 101 | estimated_total_time = elapsed_time + time_to_completion 102 | 103 | elapsed_time = TimeFormatter(milliseconds=elapsed_time) 104 | estimated_total_time = TimeFormatter(milliseconds=estimated_total_time) 105 | 106 | progress = "{0}{1}".format( 107 | ''.join(["♦" for i in range(math.floor(percentage / 10))]), 108 | ''.join(["◇" for i in range(10 - math.floor(percentage / 10))])) 109 | 110 | tmp = progress + PROGRESS_BAR.format( 111 | round(percentage, 2), 112 | humanbytes(current), 113 | humanbytes(total), 114 | humanbytes(speed), 115 | 116 | estimated_total_time if estimated_total_time != '' else "0 s" 117 | ) 118 | try: 119 | await message.edit( 120 | text="{}\n│ {}".format(ud_type, tmp),) 121 | except: 122 | pass 123 | 124 | def humanbytes(size): 125 | if not size: 126 | return "" 127 | power = 2**10 128 | n = 0 129 | Dic_powerN = {0: ' ', 1: 'K', 2: 'M', 3: 'G', 4: 'T'} 130 | while size > power: 131 | size /= power 132 | n += 1 133 | return str(round(size, 2)) + " " + Dic_powerN[n] + 'B' 134 | 135 | def TimeFormatter(milliseconds: int) -> str: 136 | seconds, milliseconds = divmod(int(milliseconds), 1000) 137 | minutes, seconds = divmod(seconds, 60) 138 | hours, minutes = divmod(minutes, 60) 139 | days, hours = divmod(hours, 24) 140 | tmp = ((str(days) + "d, ") if days else "") + \ 141 | ((str(hours) + "h, ") if hours else "") + \ 142 | ((str(minutes) + "m, ") if minutes else "") + \ 143 | ((str(seconds) + "s, ") if seconds else "") + \ 144 | ((str(milliseconds) + "ms, ") if milliseconds else "") 145 | return tmp[:-2] 146 | def convert(seconds): 147 | seconds = seconds % (24 * 3600) 148 | hour = seconds // 3600 149 | seconds %= 3600 150 | minutes = seconds // 60 151 | seconds %= 60 152 | return "%d:%02d:%02d" % (hour, minutes, seconds) 153 | async def userbot_join(userbot, invite_link): 154 | try: 155 | await userbot.join_chat(invite_link) 156 | return "Successfully joined the Channel" 157 | except UserAlreadyParticipant: 158 | return "User is already a participant." 159 | except (InviteHashInvalid, InviteHashExpired): 160 | return "Could not join. Maybe your link is expired or Invalid." 161 | except FloodWait: 162 | return "Too many requests, try again later." 163 | except Exception as e: 164 | print(e) 165 | return "Could not join, try joining manually." 166 | def get_link(string): 167 | regex = r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))" 168 | url = re.findall(regex,string) 169 | try: 170 | link = [x[0] for x in url][0] 171 | if link: 172 | return link 173 | else: 174 | return False 175 | except Exception: 176 | return False 177 | def video_metadata(file): 178 | default_values = {'width': 1, 'height': 1, 'duration': 1} 179 | try: 180 | vcap = cv2.VideoCapture(file) 181 | if not vcap.isOpened(): 182 | return default_values 183 | 184 | width = round(vcap.get(cv2.CAP_PROP_FRAME_WIDTH)) 185 | height = round(vcap.get(cv2.CAP_PROP_FRAME_HEIGHT)) 186 | fps = vcap.get(cv2.CAP_PROP_FPS) 187 | frame_count = vcap.get(cv2.CAP_PROP_FRAME_COUNT) 188 | 189 | if fps <= 0: 190 | return default_values 191 | 192 | duration = round(frame_count / fps) 193 | if duration <= 0: 194 | return default_values 195 | 196 | vcap.release() 197 | return {'width': width, 'height': height, 'duration': duration} 198 | 199 | except Exception as e: 200 | print(f"Error in video_metadata: {e}") 201 | return default_values 202 | 203 | def hhmmss(seconds): 204 | return time.strftime('%H:%M:%S',time.gmtime(seconds)) 205 | 206 | async def screenshot(video, duration, sender): 207 | if os.path.exists(f'{sender}.jpg'): 208 | return f'{sender}.jpg' 209 | time_stamp = hhmmss(int(duration)/2) 210 | out = dt.now().isoformat("_", "seconds") + ".jpg" 211 | cmd = ["ffmpeg", 212 | "-ss", 213 | f"{time_stamp}", 214 | "-i", 215 | f"{video}", 216 | "-frames:v", 217 | "1", 218 | f"{out}", 219 | "-y" 220 | ] 221 | process = await asyncio.create_subprocess_exec( 222 | *cmd, 223 | stdout=asyncio.subprocess.PIPE, 224 | stderr=asyncio.subprocess.PIPE 225 | ) 226 | stdout, stderr = await process.communicate() 227 | x = stderr.decode().strip() 228 | y = stdout.decode().strip() 229 | if os.path.isfile(out): 230 | return out 231 | else: 232 | None 233 | last_update_time = time.time() 234 | async def progress_callback(current, total, progress_message): 235 | percent = (current / total) * 100 236 | global last_update_time 237 | current_time = time.time() 238 | 239 | if current_time - last_update_time >= 10 or percent % 10 == 0: 240 | completed_blocks = int(percent // 10) 241 | remaining_blocks = 10 - completed_blocks 242 | progress_bar = "♦" * completed_blocks + "◇" * remaining_blocks 243 | current_mb = current / (1024 * 1024) 244 | total_mb = total / (1024 * 1024) 245 | await progress_message.edit( 246 | f"╭──────────────────╮\n" 247 | f"│ **__Uploading...__** \n" 248 | f"├──────────\n" 249 | f"│ {progress_bar}\n\n" 250 | f"│ **__Progress:__** {percent:.2f}%\n" 251 | f"│ **__Uploaded:__** {current_mb:.2f} MB / {total_mb:.2f} MB\n" 252 | f"╰──────────────────╯\n\n" 253 | f"**__Powered by Team SPY__**" 254 | ) 255 | 256 | last_update_time = current_time 257 | async def prog_bar(current, total, ud_type, message, start): 258 | 259 | now = time.time() 260 | diff = now - start 261 | if round(diff % 10.00) == 0 or current == total: 262 | 263 | percentage = current * 100 / total 264 | speed = current / diff 265 | elapsed_time = round(diff) * 1000 266 | time_to_completion = round((total - current) / speed) * 1000 267 | estimated_total_time = elapsed_time + time_to_completion 268 | 269 | elapsed_time = TimeFormatter(milliseconds=elapsed_time) 270 | estimated_total_time = TimeFormatter(milliseconds=estimated_total_time) 271 | 272 | progress = "{0}{1}".format( 273 | ''.join(["♦" for i in range(math.floor(percentage / 10))]), 274 | ''.join(["◇" for i in range(10 - math.floor(percentage / 10))])) 275 | 276 | tmp = progress + PROGRESS_BAR.format( 277 | round(percentage, 2), 278 | humanbytes(current), 279 | humanbytes(total), 280 | humanbytes(speed), 281 | 282 | estimated_total_time if estimated_total_time != '' else "0 s" 283 | ) 284 | try: 285 | await message.edit_text( 286 | text="{}\n│ {}".format(ud_type, tmp),) 287 | 288 | except: 289 | pass 290 | -------------------------------------------------------------------------------- /devgagan/modules/start.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: start.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | from pyrogram import filters 16 | from devgagan import app 17 | from config import OWNER_ID 18 | from devgagan.core.func import subscribe 19 | import asyncio 20 | from devgagan.core.func import * 21 | from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton 22 | from pyrogram.raw.functions.bots import SetBotInfo 23 | from pyrogram.raw.types import InputUserSelf 24 | 25 | from pyrogram.types import BotCommand, InlineKeyboardButton, InlineKeyboardMarkup 26 | 27 | @app.on_message(filters.command("set")) 28 | async def set(_, message): 29 | if message.from_user.id not in OWNER_ID: 30 | await message.reply("You are not authorized to use this command.") 31 | return 32 | 33 | await app.set_bot_commands([ 34 | BotCommand("start", "🚀 Start the bot"), 35 | BotCommand("batch", "🫠 Extract in bulk"), 36 | BotCommand("login", "🔑 Get into the bot"), 37 | BotCommand("logout", "🚪 Get out of the bot"), 38 | BotCommand("token", "🎲 Get 3 hours free access"), 39 | BotCommand("adl", "👻 Download audio from 30+ sites"), 40 | BotCommand("dl", "💀 Download videos from 30+ sites"), 41 | BotCommand("freez", "🧊 Remove all expired user"), 42 | BotCommand("pay", "₹ Pay now to get subscription"), 43 | BotCommand("status", "⟳ Refresh Payment status"), 44 | BotCommand("transfer", "💘 Gift premium to others"), 45 | BotCommand("myplan", "⌛ Get your plan details"), 46 | BotCommand("add", "➕ Add user to premium"), 47 | BotCommand("rem", "➖ Remove from premium"), 48 | BotCommand("session", "🧵 Generate Pyrogramv2 session"), 49 | BotCommand("settings", "⚙️ Personalize things"), 50 | BotCommand("stats", "📊 Get stats of the bot"), 51 | BotCommand("plan", "🗓️ Check our premium plans"), 52 | BotCommand("terms", "🥺 Terms and conditions"), 53 | BotCommand("speedtest", "🚅 Speed of server"), 54 | BotCommand("lock", "🔒 Protect channel from extraction"), 55 | BotCommand("gcast", "⚡ Broadcast message to bot users"), 56 | BotCommand("help", "❓ If you're a noob, still!"), 57 | BotCommand("cancel", "🚫 Cancel batch process") 58 | ]) 59 | 60 | await message.reply("✅ Commands configured successfully!") 61 | 62 | 63 | 64 | 65 | help_pages = [ 66 | ( 67 | "📝 **Bot Commands Overview (1/2)**:\n\n" 68 | "1. **/add userID**\n" 69 | "> Add user to premium (Owner only)\n\n" 70 | "2. **/rem userID**\n" 71 | "> Remove user from premium (Owner only)\n\n" 72 | "3. **/transfer userID**\n" 73 | "> Transfer premium to your beloved major purpose for resellers (Premium members only)\n\n" 74 | "4. **/get**\n" 75 | "> Get all user IDs (Owner only)\n\n" 76 | "5. **/lock**\n" 77 | "> Lock channel from extraction (Owner only)\n\n" 78 | "6. **/dl link**\n" 79 | "> Download videos (Not available in v3 if you are using)\n\n" 80 | "7. **/adl link**\n" 81 | "> Download audio (Not available in v3 if you are using)\n\n" 82 | "8. **/login**\n" 83 | "> Log into the bot for private channel access\n\n" 84 | "9. **/batch**\n" 85 | "> Bulk extraction for posts (After login)\n\n" 86 | ), 87 | ( 88 | "📝 **Bot Commands Overview (2/2)**:\n\n" 89 | "10. **/logout**\n" 90 | "> Logout from the bot\n\n" 91 | "11. **/stats**\n" 92 | "> Get bot stats\n\n" 93 | "12. **/plan**\n" 94 | "> Check premium plans\n\n" 95 | "13. **/speedtest**\n" 96 | "> Test the server speed (not available in v3)\n\n" 97 | "14. **/terms**\n" 98 | "> Terms and conditions\n\n" 99 | "15. **/cancel**\n" 100 | "> Cancel ongoing batch process\n\n" 101 | "16. **/myplan**\n" 102 | "> Get details about your plans\n\n" 103 | "17. **/session**\n" 104 | "> Generate Pyrogram V2 session\n\n" 105 | "18. **/settings**\n" 106 | "> 1. SETCHATID : To directly upload in channel or group or user's dm use it with -100[chatID]\n" 107 | "> 2. SETRENAME : To add custom rename tag or username of your channels\n" 108 | "> 3. CAPTION : To add custom caption\n" 109 | "> 4. REPLACEWORDS : Can be used for words in deleted set via REMOVE WORDS\n" 110 | "> 5. RESET : To set the things back to default\n\n" 111 | "> You can set CUSTOM THUMBNAIL, PDF WATERMARK, VIDEO WATERMARK, SESSION-based login, etc. from settings\n\n" 112 | "**__Powered by Team SPY__**" 113 | ) 114 | ] 115 | 116 | 117 | async def send_or_edit_help_page(_, message, page_number): 118 | if page_number < 0 or page_number >= len(help_pages): 119 | return 120 | 121 | 122 | prev_button = InlineKeyboardButton("◀️ Previous", callback_data=f"help_prev_{page_number}") 123 | next_button = InlineKeyboardButton("Next ▶️", callback_data=f"help_next_{page_number}") 124 | 125 | 126 | buttons = [] 127 | if page_number > 0: 128 | buttons.append(prev_button) 129 | if page_number < len(help_pages) - 1: 130 | buttons.append(next_button) 131 | 132 | 133 | keyboard = InlineKeyboardMarkup([buttons]) 134 | 135 | 136 | await message.delete() 137 | 138 | 139 | await message.reply( 140 | help_pages[page_number], 141 | reply_markup=keyboard 142 | ) 143 | 144 | 145 | @app.on_message(filters.command("help")) 146 | async def help(client, message): 147 | join = await subscribe(client, message) 148 | if join == 1: 149 | return 150 | 151 | 152 | await send_or_edit_help_page(client, message, 0) 153 | 154 | 155 | @app.on_callback_query(filters.regex(r"help_(prev|next)_(\d+)")) 156 | async def on_help_navigation(client, callback_query): 157 | action, page_number = callback_query.data.split("_")[1], int(callback_query.data.split("_")[2]) 158 | 159 | if action == "prev": 160 | page_number -= 1 161 | elif action == "next": 162 | page_number += 1 163 | 164 | 165 | await send_or_edit_help_page(client, callback_query.message, page_number) 166 | 167 | 168 | await callback_query.answer() 169 | 170 | 171 | from pyrogram import Client, filters 172 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 173 | 174 | @app.on_message(filters.command("terms") & filters.private) 175 | async def terms(client, message): 176 | terms_text = ( 177 | "> 📜 **Terms and Conditions** 📜\n\n" 178 | "✨ We are not responsible for user deeds, and we do not promote copyrighted content. If any user engages in such activities, it is solely their responsibility.\n" 179 | "✨ Upon purchase, we do not guarantee the uptime, downtime, or the validity of the plan. __Authorization and banning of users are at our discretion; we reserve the right to ban or authorize users at any time.__\n" 180 | "✨ Payment to us **__does not guarantee__** authorization for the /batch command. All decisions regarding authorization are made at our discretion and mood.\n" 181 | ) 182 | 183 | buttons = InlineKeyboardMarkup( 184 | [ 185 | [InlineKeyboardButton("📋 See Plans", callback_data="see_plan")], 186 | [InlineKeyboardButton("💬 Contact Now", url="https://t.me/kingofpatal")], 187 | ] 188 | ) 189 | await message.reply_text(terms_text, reply_markup=buttons) 190 | 191 | 192 | @app.on_message(filters.command("plan") & filters.private) 193 | async def plan(client, message): 194 | plan_text = ( 195 | "> 💰 **Premium Price**:\n\n Starting from $2 or 200 INR accepted via **__Amazon Gift Card__** (terms and conditions apply).\n" 196 | "📥 **Download Limit**: Users can download up to 100,000 files in a single batch command.\n" 197 | "🛑 **Batch**: You will get two modes /bulk and /batch.\n" 198 | " - Users are advised to wait for the process to automatically cancel before proceeding with any downloads or uploads.\n\n" 199 | "📜 **Terms and Conditions**: For further details and complete terms and conditions, please send /terms.\n" 200 | ) 201 | 202 | buttons = InlineKeyboardMarkup( 203 | [ 204 | [InlineKeyboardButton("📜 See Terms", callback_data="see_terms")], 205 | [InlineKeyboardButton("💬 Contact Now", url="https://t.me/kingofpatal")], 206 | ] 207 | ) 208 | await message.reply_text(plan_text, reply_markup=buttons) 209 | 210 | 211 | @app.on_callback_query(filters.regex("see_plan")) 212 | async def see_plan(client, callback_query): 213 | plan_text = ( 214 | "> 💰**Premium Price**\n\n Starting from $2 or 200 INR accepted via **__Amazon Gift Card__** (terms and conditions apply).\n" 215 | "📥 **Download Limit**: Users can download up to 100,000 files in a single batch command.\n" 216 | "🛑 **Batch**: You will get two modes /bulk and /batch.\n" 217 | " - Users are advised to wait for the process to automatically cancel before proceeding with any downloads or uploads.\n\n" 218 | "📜 **Terms and Conditions**: For further details and complete terms and conditions, please send /terms or click See Terms👇\n" 219 | ) 220 | 221 | buttons = InlineKeyboardMarkup( 222 | [ 223 | [InlineKeyboardButton("📜 See Terms", callback_data="see_terms")], 224 | [InlineKeyboardButton("💬 Contact Now", url="https://t.me/kingofpatal")], 225 | ] 226 | ) 227 | await callback_query.message.edit_text(plan_text, reply_markup=buttons) 228 | 229 | 230 | @app.on_callback_query(filters.regex("see_terms")) 231 | async def see_terms(client, callback_query): 232 | terms_text = ( 233 | "> 📜 **Terms and Conditions** 📜\n\n" 234 | "✨ We are not responsible for user deeds, and we do not promote copyrighted content. If any user engages in such activities, it is solely their responsibility.\n" 235 | "✨ Upon purchase, we do not guarantee the uptime, downtime, or the validity of the plan. __Authorization and banning of users are at our discretion; we reserve the right to ban or authorize users at any time.__\n" 236 | "✨ Payment to us **__does not guarantee__** authorization for the /batch command. All decisions regarding authorization are made at our discretion and mood.\n" 237 | ) 238 | 239 | buttons = InlineKeyboardMarkup( 240 | [ 241 | [InlineKeyboardButton("📋 See Plans", callback_data="see_plan")], 242 | [InlineKeyboardButton("💬 Contact Now", url="https://t.me/kingofpatal")], 243 | ] 244 | ) 245 | await callback_query.message.edit_text(terms_text, reply_markup=buttons) 246 | 247 | 248 | -------------------------------------------------------------------------------- /devgagan/modules/plans.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: plans.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | from datetime import timedelta 16 | import pytz 17 | import datetime, time 18 | from devgagan import app 19 | import asyncio 20 | from config import OWNER_ID 21 | from devgagan.core.func import get_seconds 22 | from devgagan.core.mongo import plans_db 23 | from pyrogram import filters 24 | 25 | 26 | 27 | @app.on_message(filters.command("rem") & filters.user(OWNER_ID)) 28 | async def remove_premium(client, message): 29 | if len(message.command) == 2: 30 | user_id = int(message.command[1]) 31 | user = await client.get_users(user_id) 32 | data = await plans_db.check_premium(user_id) 33 | 34 | if data and data.get("_id"): 35 | await plans_db.remove_premium(user_id) 36 | await message.reply_text("ᴜꜱᴇʀ ʀᴇᴍᴏᴠᴇᴅ ꜱᴜᴄᴄᴇꜱꜱꜰᴜʟʟʏ !") 37 | await client.send_message( 38 | chat_id=user_id, 39 | text=f"ʜᴇʏ {user.mention},\n\nʏᴏᴜʀ ᴘʀᴇᴍɪᴜᴍ ᴀᴄᴄᴇss ʜᴀs ʙᴇᴇɴ ʀᴇᴍᴏᴠᴇᴅ.\nᴛʜᴀɴᴋ ʏᴏᴜ ꜰᴏʀ ᴜsɪɴɢ ᴏᴜʀ sᴇʀᴠɪᴄᴇ 😊." 40 | ) 41 | else: 42 | await message.reply_text("ᴜɴᴀʙʟᴇ ᴛᴏ ʀᴇᴍᴏᴠᴇ ᴜꜱᴇᴅ !\nᴀʀᴇ ʏᴏᴜ ꜱᴜʀᴇ, ɪᴛ ᴡᴀꜱ ᴀ ᴘʀᴇᴍɪᴜᴍ ᴜꜱᴇʀ ɪᴅ ?") 43 | else: 44 | await message.reply_text("ᴜꜱᴀɢᴇ : /rem user_id") 45 | 46 | 47 | 48 | @app.on_message(filters.command("myplan")) 49 | async def myplan(client, message): 50 | user_id = message.from_user.id 51 | user = message.from_user.mention 52 | data = await plans_db.check_premium(user_id) 53 | if data and data.get("expire_date"): 54 | expiry = data.get("expire_date") 55 | expiry_ist = expiry.astimezone(pytz.timezone("Asia/Kolkata")) 56 | expiry_str_in_ist = expiry.astimezone(pytz.timezone("Asia/Kolkata")).strftime("%d-%m-%Y\n⏱️ ᴇxᴘɪʀʏ ᴛɪᴍᴇ : %I:%M:%S %p") 57 | 58 | current_time = datetime.datetime.now(pytz.timezone("Asia/Kolkata")) 59 | time_left = expiry_ist - current_time 60 | 61 | 62 | days = time_left.days 63 | hours, remainder = divmod(time_left.seconds, 3600) 64 | minutes, seconds = divmod(remainder, 60) 65 | 66 | 67 | time_left_str = f"{days} ᴅᴀʏꜱ, {hours} ʜᴏᴜʀꜱ, {minutes} ᴍɪɴᴜᴛᴇꜱ" 68 | await message.reply_text(f"⚜️ ᴘʀᴇᴍɪᴜᴍ ᴜꜱᴇʀ ᴅᴀᴛᴀ :\n\n👤 ᴜꜱᴇʀ : {user}\n⚡ ᴜꜱᴇʀ ɪᴅ : {user_id}\n⏰ ᴛɪᴍᴇ ʟᴇꜰᴛ : {time_left_str}\n⌛️ ᴇxᴘɪʀʏ ᴅᴀᴛᴇ : {expiry_str_in_ist}") 69 | else: 70 | await message.reply_text(f"ʜᴇʏ {user},\n\nʏᴏᴜ ᴅᴏ ɴᴏᴛ ʜᴀᴠᴇ ᴀɴʏ ᴀᴄᴛɪᴠᴇ ᴘʀᴇᴍɪᴜᴍ ᴘʟᴀɴs") 71 | 72 | 73 | 74 | @app.on_message(filters.command("check") & filters.user(OWNER_ID)) 75 | async def get_premium(client, message): 76 | if len(message.command) == 2: 77 | user_id = int(message.command[1]) 78 | user = await client.get_users(user_id) 79 | data = await plans_db.check_premium(user_id) 80 | if data and data.get("expire_date"): 81 | expiry = data.get("expire_date") 82 | expiry_ist = expiry.astimezone(pytz.timezone("Asia/Kolkata")) 83 | expiry_str_in_ist = expiry.astimezone(pytz.timezone("Asia/Kolkata")).strftime("%d-%m-%Y\n⏱️ ᴇxᴘɪʀʏ ᴛɪᴍᴇ : %I:%M:%S %p") 84 | 85 | current_time = datetime.datetime.now(pytz.timezone("Asia/Kolkata")) 86 | time_left = expiry_ist - current_time 87 | 88 | 89 | days = time_left.days 90 | hours, remainder = divmod(time_left.seconds, 3600) 91 | minutes, seconds = divmod(remainder, 60) 92 | 93 | 94 | time_left_str = f"{days} days, {hours} hours, {minutes} minutes" 95 | await message.reply_text(f"⚜️ ᴘʀᴇᴍɪᴜᴍ ᴜꜱᴇʀ ᴅᴀᴛᴀ :\n\n👤 ᴜꜱᴇʀ : {user.mention}\n⚡ ᴜꜱᴇʀ ɪᴅ : {user_id}\n⏰ ᴛɪᴍᴇ ʟᴇꜰᴛ : {time_left_str}\n⌛️ ᴇxᴘɪʀʏ ᴅᴀᴛᴇ : {expiry_str_in_ist}") 96 | else: 97 | await message.reply_text("ɴᴏ ᴀɴʏ ᴘʀᴇᴍɪᴜᴍ ᴅᴀᴛᴀ ᴏꜰ ᴛʜᴇ ᴡᴀꜱ ꜰᴏᴜɴᴅ ɪɴ ᴅᴀᴛᴀʙᴀꜱᴇ !") 98 | else: 99 | await message.reply_text("ᴜꜱᴀɢᴇ : /check user_id") 100 | 101 | 102 | @app.on_message(filters.command("add") & filters.user(OWNER_ID)) 103 | async def give_premium_cmd_handler(client, message): 104 | if len(message.command) == 4: 105 | time_zone = datetime.datetime.now(pytz.timezone("Asia/Kolkata")) 106 | current_time = time_zone.strftime("%d-%m-%Y\n⏱️ ᴊᴏɪɴɪɴɢ ᴛɪᴍᴇ : %I:%M:%S %p") 107 | user_id = int(message.command[1]) 108 | user = await client.get_users(user_id) 109 | time = message.command[2]+" "+message.command[3] 110 | seconds = await get_seconds(time) 111 | if seconds > 0: 112 | expiry_time = datetime.datetime.now() + datetime.timedelta(seconds=seconds) 113 | await plans_db.add_premium(user_id, expiry_time) 114 | data = await plans_db.check_premium(user_id) 115 | expiry = data.get("expire_date") 116 | expiry_str_in_ist = expiry.astimezone(pytz.timezone("Asia/Kolkata")).strftime("%d-%m-%Y\n⏱️ ᴇxᴘɪʀʏ ᴛɪᴍᴇ : %I:%M:%S %p") 117 | await message.reply_text(f"ᴘʀᴇᴍɪᴜᴍ ᴀᴅᴅᴇᴅ ꜱᴜᴄᴄᴇꜱꜱꜰᴜʟʟʏ ✅\n\n👤 ᴜꜱᴇʀ : {user.mention}\n⚡ ᴜꜱᴇʀ ɪᴅ : {user_id}\n⏰ ᴘʀᴇᴍɪᴜᴍ ᴀᴄᴄᴇꜱꜱ : {time}\n\n⏳ ᴊᴏɪɴɪɴɢ ᴅᴀᴛᴇ : {current_time}\n\n⌛️ ᴇxᴘɪʀʏ ᴅᴀᴛᴇ : {expiry_str_in_ist} \n\n__**Powered by Team SPY__**", disable_web_page_preview=True) 118 | await client.send_message( 119 | chat_id=user_id, 120 | text=f"👋 ʜᴇʏ {user.mention},\nᴛʜᴀɴᴋ ʏᴏᴜ ꜰᴏʀ ᴘᴜʀᴄʜᴀꜱɪɴɢ ᴘʀᴇᴍɪᴜᴍ.\nᴇɴᴊᴏʏ !! ✨🎉\n\n⏰ ᴘʀᴇᴍɪᴜᴍ ᴀᴄᴄᴇꜱꜱ : {time}\n⏳ ᴊᴏɪɴɪɴɢ ᴅᴀᴛᴇ : {current_time}\n\n⌛️ ᴇxᴘɪʀʏ ᴅᴀᴛᴇ : {expiry_str_in_ist}", disable_web_page_preview=True 121 | ) 122 | 123 | else: 124 | await message.reply_text("Invalid time format. Please use '1 day for days', '1 hour for hours', or '1 min for minutes', or '1 month for months' or '1 year for year'") 125 | else: 126 | await message.reply_text("Usage : /add user_id time (e.g., '1 day for days', '1 hour for hours', or '1 min for minutes', or '1 month for months' or '1 year for year')") 127 | 128 | 129 | @app.on_message(filters.command("transfer")) 130 | async def transfer_premium(client, message): 131 | if len(message.command) == 2: 132 | new_user_id = int(message.command[1]) # The user ID to whom premium is transferred 133 | sender_user_id = message.from_user.id # The current premium user issuing the command 134 | sender_user = await client.get_users(sender_user_id) 135 | new_user = await client.get_users(new_user_id) 136 | 137 | # Fetch sender's premium plan details 138 | data = await plans_db.check_premium(sender_user_id) 139 | 140 | if data and data.get("_id"): # Verify sender is already a premium user 141 | expiry = data.get("expire_date") 142 | 143 | # Remove premium for the sender 144 | await plans_db.remove_premium(sender_user_id) 145 | 146 | # Add premium for the new user with the same expiry date 147 | await plans_db.add_premium(new_user_id, expiry) 148 | 149 | # Convert expiry date to IST format for display 150 | expiry_str_in_ist = expiry.astimezone(pytz.timezone("Asia/Kolkata")).strftime( 151 | "%d-%m-%Y\n⏱️ **Expiry Time:** %I:%M:%S %p" 152 | ) 153 | time_zone = datetime.datetime.now(pytz.timezone("Asia/Kolkata")) 154 | current_time = time_zone.strftime("%d-%m-%Y\n⏱️ **Transfer Time:** %I:%M:%S %p") 155 | 156 | # Confirmation message to the sender 157 | await message.reply_text( 158 | f"✅ **Premium Plan Transferred Successfully!**\n\n" 159 | f"👤 **From:** {sender_user.mention}\n" 160 | f"👤 **To:** {new_user.mention}\n" 161 | f"⏳ **Expiry Date:** {expiry_str_in_ist}\n\n" 162 | f"__Powered by Team SPY__ 🚀" 163 | ) 164 | 165 | # Notification to the new user 166 | await client.send_message( 167 | chat_id=new_user_id, 168 | text=( 169 | f"👋 **Hey {new_user.mention},**\n\n" 170 | f"🎉 **Your Premium Plan has been Transferred!**\n" 171 | f"🛡️ **Transferred From:** {sender_user.mention}\n\n" 172 | f"⏳ **Expiry Date:** {expiry_str_in_ist}\n" 173 | f"📅 **Transferred On:** {current_time}\n\n" 174 | f"__Enjoy the Service!__ ✨" 175 | ) 176 | ) 177 | else: 178 | await message.reply_text("⚠️ **You are not a Premium user!**\n\nOnly Premium users can transfer their plans.") 179 | else: 180 | await message.reply_text("⚠️ **Usage:** /transfer user_id\n\nReplace `user_id` with the new user's ID.") 181 | 182 | 183 | async def premium_remover(): 184 | all_users = await plans_db.premium_users() 185 | removed_users = [] 186 | not_removed_users = [] 187 | 188 | for user_id in all_users: 189 | try: 190 | user = await app.get_users(user_id) 191 | chk_time = await plans_db.check_premium(user_id) 192 | 193 | if chk_time and chk_time.get("expire_date"): 194 | expiry_date = chk_time["expire_date"] 195 | 196 | if expiry_date <= datetime.datetime.now(): 197 | name = user.first_name 198 | await plans_db.remove_premium(user_id) 199 | await app.send_message(user_id, text=f"Hello {name}, your premium subscription has expired.") 200 | print(f"{name}, your premium subscription has expired.") 201 | removed_users.append(f"{name} ({user_id})") 202 | else: 203 | name = user.first_name 204 | current_time = datetime.datetime.now() 205 | time_left = expiry_date - current_time 206 | 207 | days = time_left.days 208 | hours, remainder = divmod(time_left.seconds, 3600) 209 | minutes, seconds = divmod(remainder, 60) 210 | 211 | if days > 0: 212 | remaining_time = f"{days} days, {hours} hours, {minutes} minutes, {seconds} seconds" 213 | elif hours > 0: 214 | remaining_time = f"{hours} hours, {minutes} minutes, {seconds} seconds" 215 | elif minutes > 0: 216 | remaining_time = f"{minutes} minutes, {seconds} seconds" 217 | else: 218 | remaining_time = f"{seconds} seconds" 219 | 220 | print(f"{name} : Remaining Time : {remaining_time}") 221 | not_removed_users.append(f"{name} ({user_id})") 222 | except: 223 | await plans_db.remove_premium(user_id) 224 | print(f"Unknown users captured : {user_id} removed") 225 | removed_users.append(f"Unknown ({user_id})") 226 | 227 | return removed_users, not_removed_users 228 | 229 | 230 | @app.on_message(filters.command("freez") & filters.user(OWNER_ID)) 231 | async def refresh_users(_, message): 232 | removed_users, not_removed_users = await premium_remover() 233 | # Create a summary message 234 | removed_text = "\n".join(removed_users) if removed_users else "No users removed." 235 | not_removed_text = "\n".join(not_removed_users) if not_removed_users else "No users remaining with premium." 236 | summary = ( 237 | f"**Here is Summary...**\n\n" 238 | f"> **Removed Users:**\n{removed_text}\n\n" 239 | f"> **Not Removed Users:**\n{not_removed_text}" 240 | ) 241 | await message.reply(summary) 242 | 243 | -------------------------------------------------------------------------------- /devgagan/modules/main.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: main.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # More readable 14 | # --------------------------------------------------- 15 | 16 | import time 17 | import random 18 | import string 19 | import asyncio 20 | from pyrogram import filters, Client 21 | from devgagan import app, userrbot 22 | from config import API_ID, API_HASH, FREEMIUM_LIMIT, PREMIUM_LIMIT, OWNER_ID, DEFAULT_SESSION 23 | from devgagan.core.get_func import get_msg 24 | from devgagan.core.func import * 25 | from devgagan.core.mongo import db 26 | from pyrogram.errors import FloodWait 27 | from datetime import datetime, timedelta 28 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 29 | import subprocess 30 | from devgagan.modules.shrink import is_user_verified 31 | async def generate_random_name(length=8): 32 | return ''.join(random.choices(string.ascii_lowercase, k=length)) 33 | 34 | 35 | 36 | users_loop = {} 37 | interval_set = {} 38 | batch_mode = {} 39 | 40 | async def process_and_upload_link(userbot, user_id, msg_id, link, retry_count, message): 41 | try: 42 | await get_msg(userbot, user_id, msg_id, link, retry_count, message) 43 | try: 44 | await app.delete_messages(user_id, msg_id) 45 | except Exception: 46 | pass 47 | await asyncio.sleep(15) 48 | finally: 49 | pass 50 | 51 | # Function to check if the user can proceed 52 | async def check_interval(user_id, freecheck): 53 | if freecheck != 1 or await is_user_verified(user_id): # Premium or owner users can always proceed 54 | return True, None 55 | 56 | now = datetime.now() 57 | 58 | # Check if the user is on cooldown 59 | if user_id in interval_set: 60 | cooldown_end = interval_set[user_id] 61 | if now < cooldown_end: 62 | remaining_time = (cooldown_end - now).seconds 63 | return False, f"Please wait {remaining_time} seconds(s) before sending another link. Alternatively, purchase premium for instant access.\n\n> Hey 👋 You can use /token to use the bot free for 3 hours without any time limit." 64 | else: 65 | del interval_set[user_id] # Cooldown expired, remove user from interval set 66 | 67 | return True, None 68 | 69 | async def set_interval(user_id, interval_minutes=45): 70 | now = datetime.now() 71 | # Set the cooldown interval for the user 72 | interval_set[user_id] = now + timedelta(seconds=interval_minutes) 73 | 74 | 75 | @app.on_message( 76 | filters.regex(r'https?://(?:www\.)?t\.me/[^\s]+|tg://openmessage\?user_id=\w+&message_id=\d+') 77 | & filters.private 78 | ) 79 | async def single_link(_, message): 80 | user_id = message.chat.id 81 | 82 | # Check subscription and batch mode 83 | if await subscribe(_, message) == 1 or user_id in batch_mode: 84 | return 85 | 86 | # Check if user is already in a loop 87 | if users_loop.get(user_id, False): 88 | await message.reply( 89 | "You already have an ongoing process. Please wait for it to finish or cancel it with /cancel." 90 | ) 91 | return 92 | 93 | # Check freemium limits 94 | if await chk_user(message, user_id) == 1 and FREEMIUM_LIMIT == 0 and user_id not in OWNER_ID and not await is_user_verified(user_id): 95 | await message.reply("Freemium service is currently not available. Upgrade to premium for access.") 96 | return 97 | 98 | # Check cooldown 99 | can_proceed, response_message = await check_interval(user_id, await chk_user(message, user_id)) 100 | if not can_proceed: 101 | await message.reply(response_message) 102 | return 103 | 104 | # Add user to the loop 105 | users_loop[user_id] = True 106 | 107 | link = message.text if "tg://openmessage" in message.text else get_link(message.text) 108 | msg = await message.reply("Processing...") 109 | userbot = await initialize_userbot(user_id) 110 | try: 111 | if await is_normal_tg_link(link): 112 | await process_and_upload_link(userbot, user_id, msg.id, link, 0, message) 113 | await set_interval(user_id, interval_minutes=45) 114 | else: 115 | await process_special_links(userbot, user_id, msg, link) 116 | 117 | except FloodWait as fw: 118 | await msg.edit_text(f'Try again after {fw.x} seconds due to floodwait from Telegram.') 119 | except Exception as e: 120 | await msg.edit_text(f"Link: `{link}`\n\n**Error:** {str(e)}") 121 | finally: 122 | users_loop[user_id] = False 123 | try: 124 | await msg.delete() 125 | except Exception: 126 | pass 127 | 128 | 129 | async def initialize_userbot(user_id): # this ensure the single startup .. even if logged in or not 130 | data = await db.get_data(user_id) 131 | if data and data.get("session"): 132 | try: 133 | device = 'iPhone 16 Pro' # added gareebi text 134 | userbot = Client( 135 | "userbot", 136 | api_id=API_ID, 137 | api_hash=API_HASH, 138 | device_model=device, 139 | session_string=data.get("session") 140 | ) 141 | await userbot.start() 142 | return userbot 143 | except Exception: 144 | await app.send_message(user_id, "Login Expired re do login") 145 | return None 146 | else: 147 | if DEFAULT_SESSION: 148 | return userrbot 149 | else: 150 | return None 151 | 152 | 153 | async def is_normal_tg_link(link: str) -> bool: 154 | """Check if the link is a standard Telegram link.""" 155 | special_identifiers = ['t.me/+', 't.me/c/', 't.me/b/', 'tg://openmessage'] 156 | return 't.me/' in link and not any(x in link for x in special_identifiers) 157 | 158 | async def process_special_links(userbot, user_id, msg, link): 159 | if userbot is None: 160 | return await msg.edit_text("Try logging in to the bot and try again.") 161 | if 't.me/+' in link: 162 | result = await userbot_join(userbot, link) 163 | await msg.edit_text(result) 164 | return 165 | special_patterns = ['t.me/c/', 't.me/b/', '/s/', 'tg://openmessage'] 166 | if any(sub in link for sub in special_patterns): 167 | await process_and_upload_link(userbot, user_id, msg.id, link, 0, msg) 168 | await set_interval(user_id, interval_minutes=45) 169 | return 170 | await msg.edit_text("Invalid link...") 171 | 172 | 173 | @app.on_message(filters.command("batch") & filters.private) 174 | async def batch_link(_, message): 175 | join = await subscribe(_, message) 176 | if join == 1: 177 | return 178 | user_id = message.chat.id 179 | # Check if a batch process is already running 180 | if users_loop.get(user_id, False): 181 | await app.send_message( 182 | message.chat.id, 183 | "You already have a batch process running. Please wait for it to complete." 184 | ) 185 | return 186 | 187 | freecheck = await chk_user(message, user_id) 188 | if freecheck == 1 and FREEMIUM_LIMIT == 0 and user_id not in OWNER_ID and not await is_user_verified(user_id): 189 | await message.reply("Freemium service is currently not available. Upgrade to premium for access.") 190 | return 191 | 192 | max_batch_size = FREEMIUM_LIMIT if freecheck == 1 else PREMIUM_LIMIT 193 | 194 | # Start link input 195 | for attempt in range(3): 196 | start = await app.ask(message.chat.id, "Please send the start link.\n\n> Maximum tries 3") 197 | start_id = start.text.strip() 198 | s = start_id.split("/")[-1] 199 | if s.isdigit(): 200 | cs = int(s) 201 | break 202 | await app.send_message(message.chat.id, "Invalid link. Please send again ...") 203 | else: 204 | await app.send_message(message.chat.id, "Maximum attempts exceeded. Try later.") 205 | return 206 | 207 | # Number of messages input 208 | for attempt in range(3): 209 | num_messages = await app.ask(message.chat.id, f"How many messages do you want to process?\n> Max limit {max_batch_size}") 210 | try: 211 | cl = int(num_messages.text.strip()) 212 | if 1 <= cl <= max_batch_size: 213 | break 214 | raise ValueError() 215 | except ValueError: 216 | await app.send_message( 217 | message.chat.id, 218 | f"Invalid number. Please enter a number between 1 and {max_batch_size}." 219 | ) 220 | else: 221 | await app.send_message(message.chat.id, "Maximum attempts exceeded. Try later.") 222 | return 223 | 224 | # Validate and interval check 225 | can_proceed, response_message = await check_interval(user_id, freecheck) 226 | if not can_proceed: 227 | await message.reply(response_message) 228 | return 229 | 230 | join_button = InlineKeyboardButton("Join Channel", url="https://t.me/team_spy_pro") 231 | keyboard = InlineKeyboardMarkup([[join_button]]) 232 | pin_msg = await app.send_message( 233 | user_id, 234 | f"Batch process started ⚡\nProcessing: 0/{cl}\n\n**Powered by Team SPY**", 235 | reply_markup=keyboard 236 | ) 237 | await pin_msg.pin(both_sides=True) 238 | 239 | users_loop[user_id] = True 240 | try: 241 | normal_links_handled = False 242 | userbot = await initialize_userbot(user_id) 243 | # Handle normal links first 244 | for i in range(cs, cs + cl): 245 | if user_id in users_loop and users_loop[user_id]: 246 | url = f"{'/'.join(start_id.split('/')[:-1])}/{i}" 247 | link = get_link(url) 248 | # Process t.me links (normal) without userbot 249 | if 't.me/' in link and not any(x in link for x in ['t.me/b/', 't.me/c/', 'tg://openmessage']): 250 | msg = await app.send_message(message.chat.id, f"Processing...") 251 | await process_and_upload_link(userbot, user_id, msg.id, link, 0, message) 252 | await pin_msg.edit_text( 253 | f"Batch process started ⚡\nProcessing: {i - cs + 1}/{cl}\n\n**__Powered by Team SPY__**", 254 | reply_markup=keyboard 255 | ) 256 | normal_links_handled = True 257 | if normal_links_handled: 258 | await set_interval(user_id, interval_minutes=300) 259 | await pin_msg.edit_text( 260 | f"Batch completed successfully for {cl} messages 🎉\n\n**__Powered by Team SPY__**", 261 | reply_markup=keyboard 262 | ) 263 | await app.send_message(message.chat.id, "Batch completed successfully! 🎉") 264 | return 265 | 266 | # Handle special links with userbot 267 | for i in range(cs, cs + cl): 268 | if not userbot: 269 | await app.send_message(message.chat.id, "Login in bot first ...") 270 | users_loop[user_id] = False 271 | return 272 | if user_id in users_loop and users_loop[user_id]: 273 | url = f"{'/'.join(start_id.split('/')[:-1])}/{i}" 274 | link = get_link(url) 275 | if any(x in link for x in ['t.me/b/', 't.me/c/']): 276 | msg = await app.send_message(message.chat.id, f"Processing...") 277 | await process_and_upload_link(userbot, user_id, msg.id, link, 0, message) 278 | await pin_msg.edit_text( 279 | f"Batch process started ⚡\nProcessing: {i - cs + 1}/{cl}\n\n**__Powered by Team SPY__**", 280 | reply_markup=keyboard 281 | ) 282 | 283 | await set_interval(user_id, interval_minutes=300) 284 | await pin_msg.edit_text( 285 | f"Batch completed successfully for {cl} messages 🎉\n\n**__Powered by Team SPY__**", 286 | reply_markup=keyboard 287 | ) 288 | await app.send_message(message.chat.id, "Batch completed successfully! 🎉") 289 | 290 | except Exception as e: 291 | await app.send_message(message.chat.id, f"Error: {e}") 292 | finally: 293 | users_loop.pop(user_id, None) 294 | 295 | @app.on_message(filters.command("cancel")) 296 | async def stop_batch(_, message): 297 | user_id = message.chat.id 298 | 299 | # Check if there is an active batch process for the user 300 | if user_id in users_loop and users_loop[user_id]: 301 | users_loop[user_id] = False # Set the loop status to False 302 | await app.send_message( 303 | message.chat.id, 304 | "Batch processing has been stopped successfully. You can start a new batch now if you want." 305 | ) 306 | elif user_id in users_loop and not users_loop[user_id]: 307 | await app.send_message( 308 | message.chat.id, 309 | "The batch process was already stopped. No active batch to cancel." 310 | ) 311 | else: 312 | await app.send_message( 313 | message.chat.id, 314 | "No active batch processing is running to cancel." 315 | ) 316 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Winter Release v3 3 |

4 | 5 | # Important Note : 6 | - New repo link : https://github.com/devgaganin/Save-Restricted-Content-Bot-v3/tree/main 7 | - the every branch of this repo have uniques features and all are working branches so you can migrate and test from `v1` to `v4` the `v3` is more advance than all 8 | - thanks continue fork and edit 9 | 10 | 11 | [Telegram](https://t.me/save_restricted_content_bots) | [See Recent Updates](https://github.com/devgaganin/Save-Restricted-Content-Bot-V2/tree/v3#updates) 12 | 13 | ### Star the repo it motivate us to update new features 14 | see our live bot kn telegram to check the features [Advance Content Saver Bot](https://t.me/advance_content_saver_bot) 15 | 16 | ## 📚 About This Branch 17 | - This branch is based on `Pyrogram V2` offering enhanced stability and a forced login feature. User are not forced to login in bot for public channels but for public groups and private channel they have to do login. 18 | - for detailed features scroll down to features section 19 | 20 | --- 21 | 22 | ## 🔧 Features 23 | - Extract content from both public and private channels/groups. 24 | - Rename and forward content to other channels or users. 25 | - extract restricted content from other bots how to use format link like `https://b/botusername(without @)/message_id(get it from plus messenger)` 26 | - `/login` method along with `session` based login 27 | - Custom captions and thumbnails. 28 | - Auto-remove default video thumbnails. 29 | - Delete or replace words in filenames and captions. 30 | - Auto-pin messages if enabled. 31 | - download yt/insta/Twitter/fb ect normal ytdlp supported sites that supports best format 32 | - Login via phone number. 33 | - **Supports 4GB file uploads**: The bot can handle large file uploads, up to 4GB in size. 34 | - file splitter if not premium string 35 | - **Enhanced Timer**: Distinct timers for free and paid users to limit usage and improve service. 36 | - **Improved Looping**: Optimized looping for processing multiple files or links, reducing delays and enhancing performance. 37 | - **Premium Access**: Premium users enjoy faster processing speeds and priority queue management. 38 | - ads setup shorlink ads token system 39 | - fast uploader via `SpyLib` using Telethon modules and `mautrix bridge repo` 40 | - Directly upload to `topic` in any topic enabled group 41 | 42 | 43 | ## ⚡ Commands 44 | 45 | - **`start`**: 🚀 Start the bot. 46 | - **`batch`**: 🫠 Extract in bulk. 47 | - **`login`**: 🔑 Get into the bot. 48 | - **`logout`**: 🚪 Get out of the bot. 49 | - **`token`**: 🎲 Get 3 hours of free access. 50 | - **`adl`**: 👻 Download audio from 30+ sites. 51 | - **`dl`**: 💀 Download videos from 30+ sites. 52 | - **`transfer`**: 💘 Gift premium to others. 53 | - **`myplan`**: ⌛ Get your plan details. 54 | - **`add`**: ➕ Add user to premium. 55 | - **`rem`**: ➖ Remove user from premium. 56 | - **`session`**: 🧵 Generate Pyrogramv2 session. 57 | - **`settings`**: ⚙️ Personalize settings. 58 | - **`stats`**: 📊 Get stats of the bot. 59 | - **`plan`**: 🗓️ Check our premium plans. 60 | - **`terms`**: 🥺 Terms and conditions. 61 | - **`speedtest`**: 🚅 Check the server speed. 62 | - **`get`**: 🗄️ Get all user IDs. 63 | - **`lock`**: 🔒 Protect channel from extraction. 64 | - **`gcast`**: ⚡ Broadcast message to bot users. 65 | - **`help`**: ❓ Help if you're new. 66 | - **`cancel`**: 🚫 Cancel batch process. 67 | 68 | 69 | ## ⚙️ Required Variables 70 | 71 |
72 | Click to view required variables 73 | 74 | To run the bot, you'll need to configure a few sensitive variables. Here's how to set them up securely: 75 | 76 | - **`API_ID`**: Your API ID from [telegram.org](https://my.telegram.org/auth). 77 | - **`API_HASH`**: Your API Hash from [telegram.org](https://my.telegram.org/auth). 78 | - **`BOT_TOKEN`**: Get your bot token from [@BotFather](https://t.me/botfather). 79 | - **`OWNER_ID`**: Use [@missrose_bot](https://t.me/missrose_bot) to get your user ID by sending `/info`. 80 | - **`CHANNEL_ID`**: The ID of the channel for forced subscription. 81 | - **`LOG_GROUP`**: A group or channel where the bot logs messages. Forward a message to [@userinfobot](https://t.me/userinfobot) to get your channel/group ID. 82 | - **`MONGO_DB`**: A MongoDB URL for storing session data (recommended for security). 83 | 84 | ### Additional Configuration Options: 85 | - **`STRING`**: (Optional) Add your **premium account session string** here to allow 4GB file uploads. This is **optional** and can be left empty if not used. 86 | - **`FREEMIUM_LIMIT`**: Default is `0`. Set this to any value you want to allow free users to extract content. If set to `0`, free users will not have access to any extraction features. 87 | - **`PREMIUM_LIMIT`**: Default is `500`. This is the batch limit for premium users. You can customize this to allow premium users to process more links/files in one batch. 88 | - **`YT_COOKIES`**: Yt cookies for downloading yt videos 89 | - **`INSTA_COOKIES`**: If you want to enable instagram downloading fill cookiesn 90 | 91 | **How to get cookies ??** : use mozila firfox if on android or use chrome on desktop and download extension get this cookie or any Netscape Cookies (HTTP Cookies) extractor and use that 92 | 93 | ### Monetization (Optional): 94 | - **`WEBSITE_URL`**: (Optional) This is the domain for your monetization short link service. Provide the shortener's domain name, for example: `upshrink.com`. Do **not** include `www` or `https://`. The default link shortener is already set. 95 | - **`AD_API`**: (Optional) The API key from your link shortener service (e.g., **Upshrink**, **AdFly**, etc.) to monetize links. Enter the API provided by your shortener. 96 | 97 | > **Important:** Always keep your credentials secure! Never hard-code them in the repository. Use environment variables or a `.env` file. 98 | 99 |
100 | 101 | --- 102 | 103 | ## 🚀 Deployment Guide 104 | 105 |
106 | Deploy on VPS 107 | 108 | 1. Fork the repo. 109 | 2. Update `config.py` with your values. 110 | 3. Run the following: 111 | ```bash 112 | sudo apt update 113 | sudo apt install ffmpeg git python3-pip 114 | git clone your_repo_link 115 | cd your_repo_name 116 | pip3 install -r requirements.txt 117 | python3 -m devgagan 118 | ``` 119 | 120 | - To run the bot in the background: 121 | ```bash 122 | screen -S gagan 123 | python3 -m devgagan 124 | ``` 125 | - Detach: `Ctrl + A`, then `Ctrl + D` 126 | - To stop: `screen -r gagan` and `screen -S gagan -X quit` 127 | 128 |
129 | 130 |
131 | Deploy on Heroku 132 | 133 | 1. Fork and Star the repo. 134 | 2. Click [Deploy on Heroku](https://heroku.com/deploy). 135 | 3. Enter required variables and click deploy ✅. 136 | 137 |
138 | 139 |
140 | Deploy on Render 141 | 142 | 1. Fork and star the repo. 143 | 2. Edit `config.py` or set environment variables on Render. 144 | 3. Go to [render.com](https://render.com), sign up/log in. 145 | 4. Create a new web service, select the free plan. 146 | 5. Connect your GitHub repo and deploy ✅. 147 | 148 |
149 | 150 |
151 | Deploy on Koyeb 152 | 153 | 1. Fork and star the repo. 154 | 2. Edit `config.py` or set environment variables on Koyeb. 155 | 3. Create a new service, select `Dockerfile` as build type. 156 | 4. Connect your GitHub repo and deploy ✅. 157 | 158 |
159 | 160 | --- 161 | ### ⚠️ Must Do: Secure Your Sensitive Variables 162 | 163 | **Do not expose sensitive variables (e.g., `API_ID`, `API_HASH`, `BOT_TOKEN`) on GitHub. Use environment variables to keep them secure.** 164 | 165 | ### Configuring Variables Securely: 166 | 167 | - **On VPS or Local Machine:** 168 | - Use a text editor to edit `config.py`: 169 | ```bash 170 | nano config.py 171 | ``` 172 | - Alternatively, export as environment variables: 173 | ```bash 174 | export API_ID=your_api_id 175 | export API_HASH=your_api_hash 176 | export BOT_TOKEN=your_bot_token 177 | ``` 178 | 179 | - **For Cloud Platforms (Heroku, Railway, etc.):** 180 | - Set environment variables directly in your platform’s dashboard. 181 | 182 | - **Using `.env` File:** 183 | - Create a `.env` file and add your credentials: 184 | ``` 185 | API_ID=your_api_id 186 | API_HASH=your_api_hash 187 | BOT_TOKEN=your_bot_token 188 | ``` 189 | - Make sure to add `.env` to `.gitignore` to prevent it from being pushed to GitHub. 190 | 191 | **Why This is Important?** 192 | Your credentials can be stolen if pushed to a public repository. Always keep them secure by using environment variables or local configuration files. 193 | 194 | --- 195 | 196 | ## Updates 197 |
198 | Update: 14 Feb 2025 199 | Removed forced `login` user now can proceed with invite link if they do not want to login in bot due security concerns, BOT OWNER must have to fill `DEFAULT_SESSION` var when deploying. 200 | done ✅ 201 |
202 | 203 |
204 | Update: 1 Feb 2025 205 | 206 | - Added support to upload in `topics` (in group) 207 | - seperated function from direct loop of `get_msg` and `copy_message_with_chat_id` function 208 | - & some more advancements 209 | 210 |
211 |
212 | Update: 22 Jan 2025 213 | 214 | - Added public user ID or channel story downloader support (see our tutorial for this how to save) 215 | - Renaming made asynchronous 216 | - added support for `tg://openmessage` type link for bots and users (see tutorial on channel how to use) 217 | - fixed directory type filename problems using sanitizer func 218 | - & some more advancements 219 | 220 |
221 | 222 |
223 | Update: 12 Jan 2025 224 | 225 | - Fixed blocking and stopping of bot 226 | - Fixed public topic or not topic group extraction (no need of formatting link) login required 227 | - added upload method for public group also 228 | - added `freez` command to remove the expired user with summary (auto removal of funtion is added but still if you want to execute) 229 | - rest explore the updates 230 |
231 | 232 |
233 | 234 | Update: 24 DEC 2024 235 | 236 | **1. 4GB Upload Support** 237 | - **New feature**: The bot now supports **uploading files as large as 4GB**. This is particularly useful for users working with larger media content. 238 | - **How to enable**: To allow **4GB file uploads**, you must add your **premium session string** in the `STRING` variable in the `config.py` file. This session string is only required for **premium users**. 239 | 240 | **2. New Upload Method** 241 | - A new, optimized **upload method** has been added for handling large file uploads more efficiently. 242 | - **What changed**: Previously, large files could cause slow uploads or issues. This method helps avoid those problems and ensures smoother processing. 243 | - **Note**: The upload method now handles large files seamlessly, reducing upload time and improving performance. 244 | 245 | **3. Fixed Blocking Issue** 246 | - **Resolved blocking issues**: We identified and fixed an issue that caused the bot to get blocked during the extraction or upload process, particularly when processing certain content. 247 | - **How it works now**: The bot will continue to process and extract content without interruptions or blocks, improving reliability and reducing downtime. 248 | 249 | **4. Added `/token` Method** 250 | - **New command**: A new `/token` method has been added for **short link functionality**. This command generates a token for using monetization features. 251 | - **Configuration**: 252 | - To use this feature, you must configure the **API key** and **URL** for your short link provider. 253 | - Fill in the `AD_API` (API key) and `WEBSITE_URL` (short link service domain) in the `config.py`. 254 | - **Note**: This feature is optional and only needed if you plan to use the bot for monetizing links. 255 | 256 | **5. Spylib Integration** 257 | - **Spylib added**: We have integrated **Spylib** functionality to enhance certain features. Spylib helps improve the bot’s ability to extract and handle content. 258 | - **How to set up**: For details on how to configure **Spylib**, refer to the **Spylib Code Section** in the README for a step-by-step guide. 259 | 260 | **6. Fixed Button Issues** 261 | - **Fixed broken button functionality**: There were issues where the bot’s buttons were not responding or clicking properly. This has been resolved, and now the buttons will work as expected. 262 | - **What’s fixed**: Buttons for commands like `/start`, `/help`, `/cancel`, and others should now work smoothly. 263 | **7. Added ytdlp back in this version:** 264 | - You can use command /dl or /adl for enabling this fill up ytdlp vars i.e. YT_COOKIES and INSTA_COOKIES 265 | - you have to rename `ytdl.txt` to `ytdl.py` if want to enable from `devgagan/modules/ytdl.txt` 266 | 267 | ### 🛠 Important Changes and Notes 268 | 269 | **1. Filename Deletion Behavior** 270 | - **Delete Word Behavior**: 271 | - If a word is added to the **"delete words list"**, it **will not be used in the filename**. This ensures that unwanted words are completely excluded from the filenames. 272 | - Example: If the word `deleteword` is added to the list, it will **not appear in the filename** under any circumstances. 273 | 274 | - **What’s the catch**: 275 | - The **delete word functionality** now applies specifically to **filenames only**. 276 | - For captions, you should use the **replacement method** (using spaces as a separator). 277 | 278 | **2. Deleting Words in Captions** 279 | - **How to delete words in captions**: 280 | - If you want to delete words from captions, you should use the **replacement method**, where the word will be replaced with a space (``). 281 | - This will ensure that words are replaced or deleted from captions but **not filenames**. 282 | - **Example**: 283 | - If you have the word `deleteword` in the caption, you can configure it in the replacement list like: 284 | - `'deleteword' ''`. 285 | - This will replace `deleteword` with an empty space in the caption. 286 | 287 | **3. More About the `/token` Method** 288 | - **How to use**: 289 | - After configuring the **AD_API** and **WEBSITE_URL** in the `config.py` file, use the `/token` command to generate short links. 290 | - This allows you to generate monetized links for users, where you can set up a **link shortener** (e.g., **UpShrink**, **AdFly**) and monetize the bot’s links. 291 | - **Why use it**: This is helpful for people who want to earn revenue from the links processed by the bot. It's fully configurable, and you can integrate it with any supported short link provider. 292 | 293 | **4. Other Fixes and Improvements** 294 | - **Improved handling for batch processes**: The bot now handles **batch processes** more effectively and allows users to process multiple links at once. 295 | - **Bug fixes**: Several minor bugs related to session management and batch cancellations have been addressed, ensuring a smoother user experience. 296 | 297 | **⚙️ How to Configure`. 308 | 309 |
310 | 311 |
312 | Update: 21 NOV 2024 313 | 314 | - **Public Channels**: Removed login requirement for processing links from public channels. 315 | - **Batch Size Limits**: New variables `FREEMIUM_LIMIT` and `PREMIUM_LIMIT` to manage batch sizes based on user type. 316 | - **Important Note**: Set `FREEMIUM_LIMIT` to `0` to restrict link extraction. 317 | 318 |
319 | 320 |
321 | Update: 20 NOV 2024 322 | 323 | - **Batch Processing**: Prevents overlapping batch processes. 324 | - **UserBot Management**: Safely stops `userbot` after all processes. 325 | - **Bug Fixes**: Fixed issues with `userbot` stopping and overlapping processes. 326 | 327 |
328 | 329 |
330 | Update: 16 NOV 2024 331 | 332 | - Fixed issues with `.MOV` file handling and file renaming. 333 | - Improved caption formatting. 334 | 335 |
336 | 337 |
338 | Update: 15 NOV 2024 339 | 340 | - Fixed reset button. 341 | - Added support for topic-based groups. 342 | 343 |
344 | 345 |
346 | Update: 16 AUG 2024 347 | 348 | - Added `/logout` command to clear session data. 349 | - Fixed premium membership expiration. 350 | 351 |
352 | 353 |
354 | Update: 7 JULY 2024 355 | 356 | - Introduced `/login` via phone number. 357 | - Added auto-pinning of messages and other improvements. 358 | 359 |
360 | 361 | --- 362 | ## 🛠️ Terms of Use 363 | 364 | Visit the [Terms of Use](https://github.com/devgaganin/Save-Restricted-Content-Bot-Repo/blob/master/TERMS_OF_USE.md) page to review and accept the guidelines. 365 | ## Important Note 366 | 367 | **Note**: Changing the terms and commands doesn't magically make you a developer. Real development involves understanding the code, writing new functionalities, and debugging issues, not just renaming things. If only it were that easy! 368 | 369 | ### Special thanks to: 370 | - [King of Patal](https://github.com/alreadydea) for base development of this repository. 371 | - [Mautrix Bridge](https://github.com/mautrix/telegram) for fast uploader connectivity bridge. 372 | 373 | -------------------------------------------------------------------------------- /devgagan/modules/ytdl.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: ytdl.py (pure code) 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | 16 | import yt_dlp 17 | import os 18 | import tempfile 19 | import time 20 | import asyncio 21 | import random 22 | import string 23 | import requests 24 | import logging 25 | import cv2 26 | from devgagan import sex as client 27 | from pyrogram import Client,filters 28 | from telethon import events 29 | from telethon.sync import TelegramClient 30 | from telethon.tl.types import DocumentAttributeVideo 31 | from devgagan.core.func import screenshot, video_metadata, progress_bar 32 | from telethon.tl.functions.messages import EditMessageRequest 33 | from devgagantools import fast_upload 34 | from concurrent.futures import ThreadPoolExecutor 35 | import aiohttp 36 | from devgagan import app 37 | import logging 38 | import aiofiles 39 | from mutagen.id3 import ID3, TIT2, TPE1, COMM, APIC 40 | from mutagen.mp3 import MP3 41 | 42 | logger = logging.getLogger(__name__) 43 | 44 | 45 | thread_pool = ThreadPoolExecutor() 46 | ongoing_downloads = {} 47 | 48 | def d_thumbnail(thumbnail_url, save_path): 49 | try: 50 | response = requests.get(thumbnail_url, stream=True) 51 | response.raise_for_status() 52 | with open(save_path, 'wb') as f: 53 | for chunk in response.iter_content(chunk_size=8192): 54 | f.write(chunk) 55 | return save_path 56 | except requests.exceptions.RequestException as e: 57 | logger.error(f"Failed to download thumbnail: {e}") 58 | return None 59 | 60 | 61 | async def download_thumbnail_async(url, path): 62 | async with aiohttp.ClientSession() as session: 63 | async with session.get(url) as response: 64 | if response.status == 200: 65 | with open(path, 'wb') as f: 66 | f.write(await response.read()) 67 | 68 | 69 | async def extract_audio_async(ydl_opts, url): 70 | def sync_extract(): 71 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: 72 | return ydl.extract_info(url, download=True) 73 | return await asyncio.get_event_loop().run_in_executor(thread_pool, sync_extract) 74 | 75 | 76 | def get_random_string(length=7): 77 | characters = string.ascii_letters + string.digits 78 | return ''.join(random.choice(characters) for _ in range(length)) 79 | 80 | 81 | async def process_audio(client, event, url, cookies_env_var=None): 82 | cookies = None 83 | if cookies_env_var: 84 | cookies = os.getenv(cookies_env_var) 85 | 86 | temp_cookie_path = None 87 | if cookies: 88 | with tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.txt') as temp_cookie_file: 89 | temp_cookie_file.write(cookies) 90 | temp_cookie_path = temp_cookie_file.name 91 | 92 | start_time = time.time() 93 | random_filename = f"@team_spy_pro_{event.sender_id}" 94 | download_path = f"{random_filename}.mp3" 95 | 96 | ydl_opts = { 97 | 'format': 'bestaudio/best', 98 | 'outtmpl': f"{random_filename}.%(ext)s", 99 | 'cookiefile': temp_cookie_path, 100 | 'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192'}], 101 | 'quiet': False, 102 | 'noplaylist': True, 103 | } 104 | prog = None 105 | 106 | progress_message = await event.reply("**__Starting audio extraction...__**") 107 | 108 | try: 109 | 110 | info_dict = await extract_audio_async(ydl_opts, url) 111 | title = info_dict.get('title', 'Extracted Audio') 112 | 113 | await progress_message.edit("**__Editing metadata...__**") 114 | 115 | 116 | if os.path.exists(download_path): 117 | def edit_metadata(): 118 | audio_file = MP3(download_path, ID3=ID3) 119 | try: 120 | audio_file.add_tags() 121 | except Exception: 122 | pass 123 | audio_file.tags["TIT2"] = TIT2(encoding=3, text=title) 124 | audio_file.tags["TPE1"] = TPE1(encoding=3, text="Team SPY") 125 | audio_file.tags["COMM"] = COMM(encoding=3, lang="eng", desc="Comment", text="Processed by Team SPY") 126 | 127 | thumbnail_url = info_dict.get('thumbnail') 128 | if thumbnail_url: 129 | thumbnail_path = os.path.join(tempfile.gettempdir(), "thumb.jpg") 130 | asyncio.run(download_thumbnail_async(thumbnail_url, thumbnail_path)) 131 | with open(thumbnail_path, 'rb') as img: 132 | audio_file.tags["APIC"] = APIC( 133 | encoding=3, mime='image/jpeg', type=3, desc='Cover', data=img.read() 134 | ) 135 | os.remove(thumbnail_path) 136 | audio_file.save() 137 | 138 | await asyncio.to_thread(edit_metadata) 139 | 140 | 141 | 142 | 143 | chat_id = event.chat_id 144 | if os.path.exists(download_path): 145 | await progress_message.delete() 146 | prog = await client.send_message(chat_id, "**__Starting Upload...__**") 147 | uploaded = await fast_upload( 148 | client, download_path, 149 | reply=prog, 150 | name=None, 151 | progress_bar_function=lambda done, total: progress_callback(done, total, chat_id) 152 | ) 153 | await client.send_file(chat_id, uploaded, caption=f"**{title}**\n\n**__Powered by Team SPY__**") 154 | if prog: 155 | await prog.delete() 156 | else: 157 | await event.reply("**__Audio file not found after extraction!__**") 158 | 159 | except Exception as e: 160 | logger.exception("Error during audio extraction or upload") 161 | await event.reply(f"**__An error occurred: {e}__**") 162 | finally: 163 | if os.path.exists(download_path): 164 | os.remove(download_path) 165 | if temp_cookie_path and os.path.exists(temp_cookie_path): 166 | os.remove(temp_cookie_path) 167 | 168 | @client.on(events.NewMessage(pattern="/adl")) 169 | async def handler(event): 170 | user_id = event.sender_id 171 | if user_id in ongoing_downloads: 172 | await event.reply("**You already have an ongoing download. Please wait until it completes!**") 173 | return 174 | 175 | if len(event.message.text.split()) < 2: 176 | await event.reply("**Usage:** `/adl `\n\nPlease provide a valid video link!") 177 | return 178 | 179 | url = event.message.text.split()[1] 180 | ongoing_downloads[user_id] = True 181 | 182 | try: 183 | if "instagram.com" in url: 184 | await process_audio(client, event, url, cookies_env_var="INSTA_COOKIES") 185 | elif "youtube.com" in url or "youtu.be" in url: 186 | await process_audio(client, event, url, cookies_env_var="YT_COOKIES") 187 | else: 188 | await process_audio(client, event, url) 189 | except Exception as e: 190 | await event.reply(f"**An error occurred:** `{e}`") 191 | finally: 192 | ongoing_downloads.pop(user_id, None) 193 | 194 | 195 | async def fetch_video_info(url, ydl_opts, progress_message, check_duration_and_size): 196 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: 197 | info_dict = ydl.extract_info(url, download=False) 198 | 199 | if check_duration_and_size: 200 | 201 | duration = info_dict.get('duration', 0) 202 | if duration and duration > 3 * 3600: 203 | await progress_message.edit("**❌ __Video is longer than 3 hours. Download aborted...__**") 204 | return None 205 | 206 | 207 | estimated_size = info_dict.get('filesize_approx', 0) 208 | if estimated_size and estimated_size > 2 * 1024 * 1024 * 1024: 209 | await progress_message.edit("**🤞 __Video size is larger than 2GB. Aborting download.__**") 210 | return None 211 | 212 | return info_dict 213 | 214 | def download_video(url, ydl_opts): 215 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: 216 | ydl.download([url]) 217 | 218 | 219 | @client.on(events.NewMessage(pattern="/dl")) 220 | async def handler(event): 221 | user_id = event.sender_id 222 | 223 | 224 | if user_id in ongoing_downloads: 225 | await event.reply("**You already have an ongoing ytdlp download. Please wait until it completes!**") 226 | return 227 | 228 | if len(event.message.text.split()) < 2: 229 | await event.reply("**Usage:** `/dl `\n\nPlease provide a valid video link!") 230 | return 231 | 232 | url = event.message.text.split()[1] 233 | 234 | 235 | try: 236 | if "instagram.com" in url: 237 | await process_video(client, event, url, "INSTA_COOKIES", check_duration_and_size=False) 238 | elif "youtube.com" in url or "youtu.be" in url: 239 | await process_video(client, event, url, "YT_COOKIES", check_duration_and_size=True) 240 | else: 241 | await process_video(client, event, url, None, check_duration_and_size=False) 242 | 243 | except Exception as e: 244 | await event.reply(f"**An error occurred:** `{e}`") 245 | finally: 246 | 247 | ongoing_downloads.pop(user_id, None) 248 | 249 | 250 | 251 | 252 | user_progress = {} 253 | 254 | def progress_callback(done, total, user_id): 255 | 256 | if user_id not in user_progress: 257 | user_progress[user_id] = { 258 | 'previous_done': 0, 259 | 'previous_time': time.time() 260 | } 261 | 262 | 263 | user_data = user_progress[user_id] 264 | 265 | 266 | percent = (done / total) * 100 267 | 268 | 269 | completed_blocks = int(percent // 10) 270 | remaining_blocks = 10 - completed_blocks 271 | progress_bar = "♦" * completed_blocks + "◇" * remaining_blocks 272 | 273 | 274 | done_mb = done / (1024 * 1024) 275 | total_mb = total / (1024 * 1024) 276 | 277 | 278 | speed = done - user_data['previous_done'] 279 | elapsed_time = time.time() - user_data['previous_time'] 280 | 281 | if elapsed_time > 0: 282 | speed_bps = speed / elapsed_time 283 | speed_mbps = (speed_bps * 8) / (1024 * 1024) 284 | else: 285 | speed_mbps = 0 286 | 287 | 288 | if speed_bps > 0: 289 | remaining_time = (total - done) / speed_bps 290 | else: 291 | remaining_time = 0 292 | 293 | 294 | remaining_time_min = remaining_time / 60 295 | 296 | 297 | final = ( 298 | f"╭──────────────────╮\n" 299 | f"│ **__Uploading...__** \n" 300 | f"├──────────\n" 301 | f"│ {progress_bar}\n\n" 302 | f"│ **__Progress:__** {percent:.2f}%\n" 303 | f"│ **__Done:__** {done_mb:.2f} MB / {total_mb:.2f} MB\n" 304 | f"│ **__Speed:__** {speed_mbps:.2f} Mbps\n" 305 | f"│ **__Time Remaining:__** {remaining_time_min:.2f} min\n" 306 | f"╰──────────────────╯\n\n" 307 | f"**__Powered by Team SPY__**" 308 | ) 309 | 310 | 311 | user_data['previous_done'] = done 312 | user_data['previous_time'] = time.time() 313 | 314 | return final 315 | 316 | async def process_video(client, event, url, cookies_env_var, check_duration_and_size=False): 317 | start_time = time.time() 318 | logger.info(f"Received link: {url}") 319 | 320 | cookies = None 321 | if cookies_env_var: 322 | cookies = os.getenv(cookies_env_var) 323 | 324 | 325 | random_filename = get_random_string() + ".mp4" 326 | download_path = os.path.abspath(random_filename) 327 | logger.info(f"Generated random download path: {download_path}") 328 | 329 | 330 | temp_cookie_path = None 331 | if cookies: 332 | with tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.txt') as temp_cookie_file: 333 | temp_cookie_file.write(cookies) 334 | temp_cookie_path = temp_cookie_file.name 335 | logger.info(f"Created temporary cookie file at: {temp_cookie_path}") 336 | 337 | 338 | thumbnail_file = None 339 | metadata = {'width': None, 'height': None, 'duration': None, 'thumbnail': None} 340 | 341 | 342 | ydl_opts = { 343 | 'outtmpl': download_path, 344 | 'format': 'best', 345 | 'cookiefile': temp_cookie_path if temp_cookie_path else None, 346 | 'writethumbnail': True, 347 | 'verbose': True, 348 | } 349 | prog = None 350 | progress_message = await event.reply("**__Starting download...__**") 351 | logger.info("Starting the download process...") 352 | try: 353 | info_dict = await fetch_video_info(url, ydl_opts, progress_message, check_duration_and_size) 354 | if not info_dict: 355 | return 356 | 357 | await asyncio.to_thread(download_video, url, ydl_opts) 358 | title = info_dict.get('title', 'Powered by Team SPY') 359 | k = video_metadata(download_path) 360 | W = k['width'] 361 | H = k['height'] 362 | D = k['duration'] 363 | metadata['width'] = info_dict.get('width') or W 364 | metadata['height'] = info_dict.get('height') or H 365 | metadata['duration'] = int(info_dict.get('duration') or 0) or D 366 | thumbnail_url = info_dict.get('thumbnail', None) 367 | THUMB = None 368 | 369 | 370 | if thumbnail_url: 371 | thumbnail_file = os.path.join(tempfile.gettempdir(), get_random_string() + ".jpg") 372 | downloaded_thumb = d_thumbnail(thumbnail_url, thumbnail_file) 373 | if downloaded_thumb: 374 | logger.info(f"Thumbnail saved at: {downloaded_thumb}") 375 | 376 | if thumbnail_file: 377 | THUMB = thumbnail_file 378 | else: 379 | THUMB = await screenshot(download_path, metadata['duration'], event.sender_id) 380 | 381 | 382 | 383 | 384 | chat_id = event.chat_id 385 | SIZE = 2 * 1024 * 1024 386 | caption = f"{title}" 387 | 388 | if os.path.exists(download_path) and os.path.getsize(download_path) > SIZE: 389 | prog = await client.send_message(chat_id, "**__Starting Upload...__**") 390 | await split_and_upload_file(app, chat_id, download_path, caption) 391 | await prog.delete() 392 | 393 | if os.path.exists(download_path): 394 | await progress_message.delete() 395 | prog = await client.send_message(chat_id, "**__Starting Upload...__**") 396 | uploaded = await fast_upload( 397 | client, download_path, 398 | reply=prog, 399 | progress_bar_function=lambda done, total: progress_callback(done, total, chat_id) 400 | ) 401 | await client.send_file( 402 | event.chat_id, 403 | uploaded, 404 | caption=f"**{title}**", 405 | attributes=[ 406 | DocumentAttributeVideo( 407 | duration=metadata['duration'], 408 | w=metadata['width'], 409 | h=metadata['height'], 410 | supports_streaming=True 411 | ) 412 | ], 413 | thumb=THUMB if THUMB else None 414 | ) 415 | if prog: 416 | await prog.delete() 417 | else: 418 | await event.reply("**__File not found after download. Something went wrong!__**") 419 | except Exception as e: 420 | logger.exception("An error occurred during download or upload.") 421 | await event.reply(f"**__An error occurred: {e}__**") 422 | finally: 423 | 424 | if os.path.exists(download_path): 425 | os.remove(download_path) 426 | if temp_cookie_path and os.path.exists(temp_cookie_path): 427 | os.remove(temp_cookie_path) 428 | if thumbnail_file and os.path.exists(thumbnail_file): 429 | os.remove(thumbnail_file) 430 | 431 | 432 | async def split_and_upload_file(app, sender, file_path, caption): 433 | if not os.path.exists(file_path): 434 | await app.send_message(sender, "❌ File not found!") 435 | return 436 | 437 | file_size = os.path.getsize(file_path) 438 | start = await app.send_message(sender, f"ℹ️ File size: {file_size / (1024 * 1024):.2f} MB") 439 | PART_SIZE = 1.9 * 1024 * 1024 * 1024 440 | 441 | part_number = 0 442 | async with aiofiles.open(file_path, mode="rb") as f: 443 | while True: 444 | chunk = await f.read(PART_SIZE) 445 | if not chunk: 446 | break 447 | 448 | # Create part filename 449 | base_name, file_ext = os.path.splitext(file_path) 450 | part_file = f"{base_name}.part{str(part_number).zfill(3)}{file_ext}" 451 | 452 | # Write part to file 453 | async with aiofiles.open(part_file, mode="wb") as part_f: 454 | await part_f.write(chunk) 455 | 456 | # Uploading part 457 | edit = await app.send_message(sender, f"⬆️ Uploading part {part_number + 1}...") 458 | part_caption = f"{caption} \n\n**Part : {part_number + 1}**" 459 | await app.send_document(sender, document=part_file, caption=part_caption, 460 | progress=progress_bar, 461 | progress_args=("╭─────────────────────╮\n│ **__Pyro Uploader__**\n├─────────────────────", edit, time.time()) 462 | ) 463 | await edit.delete() 464 | os.remove(part_file) # Cleanup after upload 465 | 466 | part_number += 1 467 | 468 | await start.delete() 469 | os.remove(file_path) 470 | 471 | -------------------------------------------------------------------------------- /devgagan/core/get_func.py: -------------------------------------------------------------------------------- 1 | # --------------------------------------------------- 2 | # File Name: get_func.py 3 | # Description: A Pyrogram bot for downloading files from Telegram channels or groups 4 | # and uploading them back to Telegram. 5 | # Author: Gagan 6 | # GitHub: https://github.com/devgaganin/ 7 | # Telegram: https://t.me/team_spy_pro 8 | # YouTube: https://youtube.com/@dev_gagan 9 | # Created: 2025-01-11 10 | # Last Modified: 2025-01-11 11 | # Version: 2.0.5 12 | # License: MIT License 13 | # --------------------------------------------------- 14 | 15 | import asyncio 16 | import os 17 | import re 18 | import time 19 | import gc 20 | from typing import Dict, Set, Optional, Union, Any, Tuple, List 21 | from pathlib import Path 22 | from functools import lru_cache, wraps 23 | from collections import defaultdict 24 | from dataclasses import dataclass, field 25 | from contextlib import asynccontextmanager 26 | import aiofiles 27 | import pymongo 28 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Message 29 | from pyrogram.errors import ChannelBanned, ChannelInvalid, ChannelPrivate, ChatIdInvalid, ChatInvalid, RPCError 30 | from pyrogram.enums import MessageMediaType, ParseMode 31 | from telethon.tl.types import DocumentAttributeVideo 32 | from telethon import events, Button 33 | from devgagan import app, sex as gf 34 | from devgagan.core.func import * 35 | from devgagan.core.mongo import db as odb 36 | from devgagantools import fast_upload 37 | from config import MONGO_DB as MONGODB_CONNECTION_STRING, LOG_GROUP, OWNER_ID, STRING, API_ID, API_HASH 38 | 39 | # Import pro userbot if STRING is available 40 | if STRING: 41 | from devgagan import pro 42 | else: 43 | pro = None 44 | 45 | # Constants and Configuration 46 | @dataclass 47 | class BotConfig: 48 | DB_NAME: str = "smart_users" 49 | COLLECTION_NAME: str = "super_user" 50 | VIDEO_EXTS: Set[str] = field(default_factory=lambda: { 51 | 'mp4', 'mov', 'avi', 'mkv', 'flv', 'wmv', 'webm', 'mpg', 'mpeg', 52 | '3gp', 'ts', 'm4v', 'f4v', 'vob' 53 | }) 54 | DOC_EXTS: Set[str] = field(default_factory=lambda: { 55 | 'pdf', 'docx', 'txt', 'epub', 'docs' 56 | }) 57 | IMG_EXTS: Set[str] = field(default_factory=lambda: { 58 | 'jpg', 'jpeg', 'png', 'webp' 59 | }) 60 | AUDIO_EXTS: Set[str] = field(default_factory=lambda: { 61 | 'mp3', 'wav', 'flac', 'aac', 'm4a', 'ogg' 62 | }) 63 | SIZE_LIMIT: int = 2 * 1024**3 # 2GB 64 | PART_SIZE: int = int(1.9 * 1024**3) # 1.9GB for splitting 65 | SETTINGS_PIC: str = "settings.jpg" 66 | 67 | @dataclass 68 | class UserProgress: 69 | previous_done: int = 0 70 | previous_time: float = field(default_factory=time.time) 71 | 72 | class DatabaseManager: 73 | """Enhanced database operations with error handling and caching""" 74 | def __init__(self, connection_string: str, db_name: str, collection_name: str): 75 | self.client = pymongo.MongoClient(connection_string) 76 | self.collection = self.client[db_name][collection_name] 77 | self._cache = {} 78 | 79 | def get_user_data(self, user_id: int, key: str, default=None) -> Any: 80 | cache_key = f"{user_id}:{key}" 81 | if cache_key in self._cache: 82 | return self._cache[cache_key] 83 | 84 | try: 85 | doc = self.collection.find_one({"_id": user_id}) 86 | value = doc.get(key, default) if doc else default 87 | self._cache[cache_key] = value 88 | return value 89 | except Exception as e: 90 | print(f"Database read error: {e}") 91 | return default 92 | 93 | def save_user_data(self, user_id: int, key: str, value: Any) -> bool: 94 | cache_key = f"{user_id}:{key}" 95 | try: 96 | self.collection.update_one( 97 | {"_id": user_id}, 98 | {"$set": {key: value}}, 99 | upsert=True 100 | ) 101 | self._cache[cache_key] = value 102 | return True 103 | except Exception as e: 104 | print(f"Database save error for {key}: {e}") 105 | return False 106 | 107 | def clear_user_cache(self, user_id: int): 108 | """Clear cache for specific user""" 109 | keys_to_remove = [key for key in self._cache.keys() if key.startswith(f"{user_id}:")] 110 | for key in keys_to_remove: 111 | del self._cache[key] 112 | 113 | def get_protected_channels(self) -> Set[int]: 114 | try: 115 | return {doc["channel_id"] for doc in self.collection.find({"channel_id": {"$exists": True}})} 116 | except: 117 | return set() 118 | 119 | def lock_channel(self, channel_id: int) -> bool: 120 | try: 121 | self.collection.insert_one({"channel_id": channel_id}) 122 | return True 123 | except: 124 | return False 125 | 126 | def reset_user_data(self, user_id: int) -> bool: 127 | try: 128 | self.collection.update_one( 129 | {"_id": user_id}, 130 | {"$unset": { 131 | "delete_words": "", "replacement_words": "", 132 | "watermark_text": "", "duration_limit": "", 133 | "custom_caption": "", "rename_tag": "" 134 | }} 135 | ) 136 | self.clear_user_cache(user_id) 137 | return True 138 | except Exception as e: 139 | print(f"Reset error: {e}") 140 | return False 141 | 142 | class MediaProcessor: 143 | """Advanced media processing and file type detection""" 144 | def __init__(self, config: BotConfig): 145 | self.config = config 146 | 147 | def get_file_type(self, filename: str) -> str: 148 | """Determine file type based on extension""" 149 | ext = Path(filename).suffix.lower().lstrip('.') 150 | if ext in self.config.VIDEO_EXTS: 151 | return 'video' 152 | elif ext in self.config.IMG_EXTS: 153 | return 'photo' 154 | elif ext in self.config.AUDIO_EXTS: 155 | return 'audio' 156 | elif ext in self.config.DOC_EXTS: 157 | return 'document' 158 | return 'document' 159 | 160 | @staticmethod 161 | def get_media_info(msg) -> Tuple[Optional[str], Optional[int], str]: 162 | """Extract filename, file size, and media type from message""" 163 | if msg.document: 164 | return msg.document.file_name or "document", msg.document.file_size, "document" 165 | elif msg.video: 166 | return msg.video.file_name or "video.mp4", msg.video.file_size, "video" 167 | elif msg.photo: 168 | return "photo.jpg", msg.photo.file_size, "photo" 169 | elif msg.audio: 170 | return msg.audio.file_name or "audio.mp3", msg.audio.file_size, "audio" 171 | elif msg.voice: 172 | return "voice.ogg", getattr(msg.voice, 'file_size', 1), "voice" 173 | elif msg.video_note: 174 | return "video_note.mp4", getattr(msg.video_note, 'file_size', 1), "video_note" 175 | elif msg.sticker: 176 | return "sticker.webp", getattr(msg.sticker, 'file_size', 1), "sticker" 177 | return "unknown", 1, "document" 178 | 179 | class ProgressManager: 180 | """Enhanced progress tracking with better formatting""" 181 | def __init__(self): 182 | self.user_progress: Dict[int, UserProgress] = defaultdict(UserProgress) 183 | 184 | def calculate_progress(self, done: int, total: int, user_id: int, uploader: str = "SpyLib") -> str: 185 | user_data = self.user_progress[user_id] 186 | percent = (done / total) * 100 187 | progress_bar = "♦" * int(percent // 10) + "◇" * (10 - int(percent // 10)) 188 | done_mb, total_mb = done / (1024**2), total / (1024**2) 189 | 190 | # Calculate speed and ETA 191 | speed = max(0, done - user_data.previous_done) 192 | elapsed_time = max(0.1, time.time() - user_data.previous_time) 193 | speed_mbps = (speed * 8) / (1024**2 * elapsed_time) if elapsed_time > 0 else 0 194 | eta_seconds = ((total - done) / speed) if speed > 0 else 0 195 | eta_min = eta_seconds / 60 196 | 197 | # Update progress 198 | user_data.previous_done = done 199 | user_data.previous_time = time.time() 200 | 201 | return ( 202 | f"╭──────────────────╮\n" 203 | f"│ **__{uploader} ⚡ Uploader__**\n" 204 | f"├──────────\n" 205 | f"│ {progress_bar}\n\n" 206 | f"│ **__Progress:__** {percent:.2f}%\n" 207 | f"│ **__Done:__** {done_mb:.2f} MB / {total_mb:.2f} MB\n" 208 | f"│ **__Speed:__** {speed_mbps:.2f} Mbps\n" 209 | f"│ **__ETA:__** {eta_min:.2f} min\n" 210 | f"╰──────────────────╯\n\n" 211 | f"**__Powered by Team SPY__**" 212 | ) 213 | 214 | class CaptionFormatter: 215 | """Advanced caption processing with markdown to HTML conversion""" 216 | 217 | @staticmethod 218 | async def markdown_to_html(caption: str) -> str: 219 | """Convert markdown formatting to HTML""" 220 | if not caption: 221 | return "" 222 | 223 | replacements = [ 224 | (r"^> (.*)", r"
\1
"), 225 | (r"```(.*?)```", r"
\1
"), 226 | (r"`(.*?)`", r"\1"), 227 | (r"\*\*(.*?)\*\*", r"\1"), 228 | (r"\*(.*?)\*", r"\1"), 229 | (r"__(.*?)__", r"\1"), 230 | (r"_(.*?)_", r"\1"), 231 | (r"~~(.*?)~~", r"\1"), 232 | (r"\|\|(.*?)\|\|", r"
\1
"), 233 | (r"\[(.*?)\]\((.*?)\)", r'\1') 234 | ] 235 | 236 | result = caption 237 | for pattern, replacement in replacements: 238 | result = re.sub(pattern, replacement, result, flags=re.MULTILINE | re.DOTALL) 239 | 240 | return result.strip() 241 | 242 | class FileOperations: 243 | """File operations with enhanced error handling""" 244 | def __init__(self, config: BotConfig, db: DatabaseManager): 245 | self.config = config 246 | self.db = db 247 | 248 | @asynccontextmanager 249 | async def safe_file_operation(self, file_path: str): 250 | """Safe file operations with automatic cleanup""" 251 | try: 252 | yield file_path 253 | finally: 254 | await self._cleanup_file(file_path) 255 | 256 | async def _cleanup_file(self, file_path: str): 257 | """Safely remove file""" 258 | if file_path and os.path.exists(file_path): 259 | try: 260 | await asyncio.to_thread(os.remove, file_path) 261 | except Exception as e: 262 | print(f"Error removing file {file_path}: {e}") 263 | 264 | async def process_filename(self, file_path: str, user_id: int) -> str: 265 | """Process filename with user preferences""" 266 | delete_words = set(self.db.get_user_data(user_id, "delete_words", [])) 267 | replacements = self.db.get_user_data(user_id, "replacement_words", {}) 268 | rename_tag = self.db.get_user_data(user_id, "rename_tag", "Team SPY") 269 | 270 | path = Path(file_path) 271 | name = path.stem 272 | extension = path.suffix.lstrip('.') 273 | 274 | # Process filename 275 | for word in delete_words: 276 | name = name.replace(word, "") 277 | 278 | for word, replacement in replacements.items(): 279 | name = name.replace(word, replacement) 280 | 281 | # Normalize extension for videos 282 | if extension.lower() in self.config.VIDEO_EXTS and extension.lower() not in ['mp4']: 283 | extension = 'mp4' 284 | 285 | new_name = f"{name.strip()} {rename_tag}.{extension}" 286 | new_path = path.parent / new_name 287 | 288 | await asyncio.to_thread(os.rename, file_path, new_path) 289 | return str(new_path) 290 | 291 | async def split_large_file(self, file_path: str, app_client, sender: int, target_chat_id: int, caption: str, topic_id: Optional[int] = None): 292 | """Split large files into smaller parts""" 293 | if not os.path.exists(file_path): 294 | await app_client.send_message(sender, "❌ File not found!") 295 | return 296 | 297 | file_size = os.path.getsize(file_path) 298 | start_msg = await app_client.send_message( 299 | sender, f"ℹ️ File size: {file_size / (1024**2):.2f} MB\n🔄 Splitting and uploading..." 300 | ) 301 | 302 | part_number = 0 303 | base_path = Path(file_path) 304 | 305 | try: 306 | async with aiofiles.open(file_path, mode="rb") as f: 307 | while True: 308 | chunk = await f.read(self.config.PART_SIZE) 309 | if not chunk: 310 | break 311 | 312 | part_file = f"{base_path.stem}.part{str(part_number).zfill(3)}{base_path.suffix}" 313 | 314 | async with aiofiles.open(part_file, mode="wb") as part_f: 315 | await part_f.write(chunk) 316 | 317 | part_caption = f"{caption}\n\n**Part: {part_number + 1}**" if caption else f"**Part: {part_number + 1}**" 318 | 319 | edit_msg = await app_client.send_message(target_chat_id, f"⬆️ Uploading part {part_number + 1}...") 320 | 321 | try: 322 | result = await app_client.send_document( 323 | target_chat_id, 324 | document=part_file, 325 | caption=part_caption, 326 | reply_to_message_id=topic_id, 327 | progress=progress_bar, 328 | progress_args=("╭──────────────╮\n│ **__Pyro Uploader__**\n├────────", edit_msg, time.time()) 329 | ) 330 | await result.copy(LOG_GROUP) 331 | await edit_msg.delete() 332 | finally: 333 | if os.path.exists(part_file): 334 | os.remove(part_file) 335 | 336 | part_number += 1 337 | 338 | finally: 339 | await start_msg.delete() 340 | if os.path.exists(file_path): 341 | os.remove(file_path) 342 | 343 | class SmartTelegramBot: 344 | """Main bot class with all functionality""" 345 | def __init__(self): 346 | self.config = BotConfig() 347 | self.db = DatabaseManager(MONGODB_CONNECTION_STRING, self.config.DB_NAME, self.config.COLLECTION_NAME) 348 | self.media_processor = MediaProcessor(self.config) 349 | self.progress_manager = ProgressManager() 350 | self.file_ops = FileOperations(self.config, self.db) 351 | self.caption_formatter = CaptionFormatter() 352 | 353 | # User session management 354 | self.user_sessions: Dict[int, str] = {} 355 | self.pending_photos: Set[int] = set() 356 | self.user_chat_ids: Dict[int, str] = {} 357 | self.user_rename_prefs: Dict[str, str] = {} 358 | self.user_caption_prefs: Dict[str, str] = {} 359 | 360 | # Pro userbot reference 361 | self.pro_client = pro 362 | print(f"Pro client available: {'Yes' if self.pro_client else 'No'}") 363 | 364 | def get_thumbnail_path(self, user_id: int) -> Optional[str]: 365 | """Get user's custom thumbnail path""" 366 | thumb_path = f'{user_id}.jpg' 367 | return thumb_path if os.path.exists(thumb_path) else None 368 | 369 | def parse_target_chat(self, target: str) -> Tuple[int, Optional[int]]: 370 | """Parse chat ID and topic ID from target string""" 371 | if '/' in target: 372 | parts = target.split('/') 373 | return int(parts[0]), int(parts[1]) 374 | return int(target), None 375 | 376 | async def process_user_caption(self, original_caption: str, user_id: int) -> str: 377 | """Process caption with user preferences""" 378 | custom_caption = self.user_caption_prefs.get(str(user_id), "") or self.db.get_user_data(user_id, "custom_caption", "") 379 | delete_words = set(self.db.get_user_data(user_id, "delete_words", [])) 380 | replacements = self.db.get_user_data(user_id, "replacement_words", {}) 381 | 382 | # Process original caption 383 | processed = original_caption or "" 384 | 385 | # Remove delete words 386 | for word in delete_words: 387 | processed = processed.replace(word, "") 388 | 389 | # Apply replacements 390 | for word, replacement in replacements.items(): 391 | processed = processed.replace(word, replacement) 392 | 393 | # Add custom caption 394 | if custom_caption: 395 | processed = f"{processed}\n\n{custom_caption}".strip() 396 | 397 | return processed if processed else None 398 | 399 | async def upload_with_pyrogram(self, file_path: str, user_id: int, target_chat_id: int, caption: str, topic_id: Optional[int] = None, edit_msg=None): 400 | """Upload using Pyrogram with proper file type detection""" 401 | file_type = self.media_processor.get_file_type(file_path) 402 | thumb_path = self.get_thumbnail_path(user_id) 403 | 404 | progress_args = ("╭──────────────╮\n│ **__Pyro Uploader__**\n├────────", edit_msg, time.time()) 405 | 406 | try: 407 | if file_type == 'video': 408 | # Get video metadata 409 | metadata = {} 410 | if 'video_metadata' in globals(): 411 | metadata = video_metadata(file_path) 412 | 413 | width = metadata.get('width', 0) 414 | height = metadata.get('height', 0) 415 | duration = metadata.get('duration', 0) 416 | 417 | # Generate thumbnail if not exists 418 | if not thumb_path and 'screenshot' in globals(): 419 | try: 420 | thumb_path = await screenshot(file_path, duration, user_id) 421 | except: 422 | pass 423 | 424 | result = await app.send_video( 425 | chat_id=target_chat_id, 426 | video=file_path, 427 | caption=caption, 428 | height=height, 429 | width=width, 430 | duration=duration, 431 | thumb=thumb_path, 432 | reply_to_message_id=topic_id, 433 | parse_mode=ParseMode.MARKDOWN, 434 | progress=progress_bar, 435 | progress_args=progress_args 436 | ) 437 | 438 | elif file_type == 'photo': 439 | result = await app.send_photo( 440 | chat_id=target_chat_id, 441 | photo=file_path, 442 | caption=caption, 443 | reply_to_message_id=topic_id, 444 | parse_mode=ParseMode.MARKDOWN, 445 | progress=progress_bar, 446 | progress_args=progress_args 447 | ) 448 | 449 | elif file_type == 'audio': 450 | result = await app.send_audio( 451 | chat_id=target_chat_id, 452 | audio=file_path, 453 | caption=caption, 454 | reply_to_message_id=topic_id, 455 | parse_mode=ParseMode.MARKDOWN, 456 | progress=progress_bar, 457 | progress_args=progress_args 458 | ) 459 | 460 | else: # document 461 | result = await app.send_document( 462 | chat_id=target_chat_id, 463 | document=file_path, 464 | caption=caption, 465 | thumb=thumb_path, 466 | reply_to_message_id=topic_id, 467 | parse_mode=ParseMode.MARKDOWN, 468 | progress=progress_bar, 469 | progress_args=progress_args 470 | ) 471 | 472 | # Copy to log group 473 | await result.copy(LOG_GROUP) 474 | return result 475 | 476 | except Exception as e: 477 | await app.send_message(LOG_GROUP, f"**Pyrogram Upload Failed:** {str(e)}") 478 | raise 479 | finally: 480 | if edit_msg: 481 | try: 482 | await edit_msg.delete() 483 | except: 484 | pass 485 | 486 | async def upload_with_telethon(self, file_path: str, user_id: int, target_chat_id: int, caption: str, topic_id: Optional[int] = None, edit_msg=None): 487 | """Upload using Telethon (SpyLib) with enhanced features""" 488 | try: 489 | if edit_msg: 490 | await edit_msg.delete() 491 | 492 | progress_message = await gf.send_message(user_id, "**__SpyLib ⚡ Uploading...__**") 493 | html_caption = await self.caption_formatter.markdown_to_html(caption) 494 | 495 | # Upload file using fast_upload 496 | uploaded = await fast_upload( 497 | gf, file_path, 498 | reply=progress_message, 499 | name=None, 500 | progress_bar_function=lambda done, total: self.progress_manager.calculate_progress(done, total, user_id, "SpyLib"), 501 | user_id=user_id 502 | ) 503 | 504 | await progress_message.delete() 505 | 506 | # Prepare attributes based on file type 507 | attributes = [] 508 | file_type = self.media_processor.get_file_type(file_path) 509 | 510 | if file_type == 'video': 511 | if 'video_metadata' in globals(): 512 | metadata = video_metadata(file_path) 513 | duration = metadata.get('duration', 0) 514 | width = metadata.get('width', 0) 515 | height = metadata.get('height', 0) 516 | attributes = [DocumentAttributeVideo( 517 | duration=duration, w=width, h=height, supports_streaming=True 518 | )] 519 | 520 | thumb_path = self.get_thumbnail_path(user_id) 521 | 522 | # Send to target chat 523 | await gf.send_file( 524 | target_chat_id, 525 | uploaded, 526 | caption=html_caption, 527 | attributes=attributes, 528 | reply_to=topic_id, 529 | parse_mode='html', 530 | thumb=thumb_path 531 | ) 532 | 533 | # Send to log group 534 | await gf.send_file( 535 | LOG_GROUP, 536 | uploaded, 537 | caption=html_caption, 538 | attributes=attributes, 539 | parse_mode='html', 540 | thumb=thumb_path 541 | ) 542 | 543 | except Exception as e: 544 | await app.send_message(LOG_GROUP, f"**SpyLib Upload Failed:** {str(e)}") 545 | raise 546 | 547 | async def handle_large_file_upload(self, file_path: str, sender: int, edit_msg, caption: str): 548 | """Handle files larger than 2GB using pro client""" 549 | if not self.pro_client: 550 | await edit_msg.edit('**❌ 4GB upload not available - Pro client not configured**') 551 | return 552 | 553 | await edit_msg.edit('**✅ 4GB upload starting...**') 554 | 555 | target_chat_str = self.user_chat_ids.get(sender, str(sender)) 556 | target_chat_id, _ = self.parse_target_chat(target_chat_str) 557 | 558 | file_type = self.media_processor.get_file_type(file_path) 559 | thumb_path = self.get_thumbnail_path(sender) 560 | 561 | progress_args = ("╭──────────────╮\n│ **__4GB Uploader ⚡__**\n├────────", edit_msg, time.time()) 562 | 563 | try: 564 | if file_type == 'video': 565 | metadata = {} 566 | if 'video_metadata' in globals(): 567 | metadata = video_metadata(file_path) 568 | 569 | result = await self.pro_client.send_video( 570 | LOG_GROUP, 571 | video=file_path, 572 | caption=caption, 573 | thumb=thumb_path, 574 | height=metadata.get('height', 0), 575 | width=metadata.get('width', 0), 576 | duration=metadata.get('duration', 0), 577 | progress=progress_bar, 578 | progress_args=progress_args 579 | ) 580 | else: 581 | result = await self.pro_client.send_document( 582 | LOG_GROUP, 583 | document=file_path, 584 | caption=caption, 585 | thumb=thumb_path, 586 | progress=progress_bar, 587 | progress_args=progress_args 588 | ) 589 | 590 | # Check if user is premium or free 591 | free_check = 0 592 | if 'chk_user' in globals(): 593 | free_check = await chk_user(sender, sender) 594 | 595 | if free_check == 1: 596 | # Free user - send with protection 597 | reply_markup = InlineKeyboardMarkup([[ 598 | InlineKeyboardButton("💎 Get Premium to Forward", url="https://t.me/kingofpatal") 599 | ]]) 600 | await app.copy_message(target_chat_id, LOG_GROUP, result.id, protect_content=True, reply_markup=reply_markup) 601 | else: 602 | # Premium user - send normally 603 | await app.copy_message(target_chat_id, LOG_GROUP, result.id) 604 | 605 | except Exception as e: 606 | print(f"Large file upload error: {e}") 607 | await app.send_message(LOG_GROUP, f"**4GB Upload Error:** {str(e)}") 608 | finally: 609 | await edit_msg.delete() 610 | 611 | async def handle_message_download(self, userbot, sender: int, edit_id: int, msg_link: str, offset: int, message): 612 | """Main message processing function with enhanced error handling""" 613 | edit_msg = None 614 | file_path = None 615 | 616 | try: 617 | # Parse and validate message link 618 | msg_link = msg_link.split("?single")[0] 619 | protected_channels = self.db.get_protected_channels() 620 | 621 | # Extract chat and message info 622 | chat_id, msg_id = await self._parse_message_link(msg_link, offset, protected_channels, sender, edit_id) 623 | if not chat_id: 624 | return 625 | 626 | # Get target chat configuration 627 | target_chat_str = self.user_chat_ids.get(message.chat.id, str(message.chat.id)) 628 | target_chat_id, topic_id = self.parse_target_chat(target_chat_str) 629 | 630 | # Fetch message 631 | msg = await userbot.get_messages(chat_id, msg_id) 632 | if not msg or msg.service or msg.empty: 633 | await app.delete_messages(sender, edit_id) 634 | return 635 | 636 | # Handle special message types 637 | if await self._handle_special_messages(msg, target_chat_id, topic_id, edit_id, sender): 638 | return 639 | 640 | # Process media files 641 | if not msg.media: 642 | return 643 | 644 | filename, file_size, media_type = self.media_processor.get_media_info(msg) 645 | 646 | # Handle direct media types (voice, video_note, sticker) 647 | if await self._handle_direct_media(msg, target_chat_id, topic_id, edit_id, media_type): 648 | return 649 | 650 | # Download file 651 | edit_msg = await app.edit_message_text(sender, edit_id, "**📥 Downloading...**") 652 | 653 | progress_args = ("╭──────────────╮\n│ **__Downloading...__**\n├────────", edit_msg, time.time()) 654 | file_path = await userbot.download_media( 655 | msg, file_name=filename, progress=progress_bar, progress_args=progress_args 656 | ) 657 | 658 | # Process caption and filename 659 | caption = await self.process_user_caption(msg.caption.markdown if msg.caption else "", sender) 660 | file_path = await self.file_ops.process_filename(file_path, sender) 661 | 662 | # Handle photos separately 663 | if media_type == "photo": 664 | result = await app.send_photo(target_chat_id, file_path, caption=caption, reply_to_message_id=topic_id) 665 | await result.copy(LOG_GROUP) 666 | await edit_msg.delete() 667 | return 668 | 669 | # Check file size and handle accordingly 670 | upload_method = self.db.get_user_data(sender, "upload_method", "Pyrogram") 671 | 672 | if file_size > self.config.SIZE_LIMIT: 673 | free_check = 0 674 | if 'chk_user' in globals(): 675 | free_check = await chk_user(chat_id, sender) 676 | 677 | if free_check == 1 or not self.pro_client: 678 | # Split file for free users or when pro client unavailable 679 | await edit_msg.delete() 680 | await self.file_ops.split_large_file(file_path, app, sender, target_chat_id, caption, topic_id) 681 | return 682 | else: 683 | # Use 4GB uploader 684 | await self.handle_large_file_upload(file_path, sender, edit_msg, caption) 685 | return 686 | 687 | # Regular upload 688 | if upload_method == "Telethon" and gf: 689 | await self.upload_with_telethon(file_path, sender, target_chat_id, caption, topic_id, edit_msg) 690 | else: 691 | await self.upload_with_pyrogram(file_path, sender, target_chat_id, caption, topic_id, edit_msg) 692 | 693 | except (ChannelBanned, ChannelInvalid, ChannelPrivate, ChatIdInvalid, ChatInvalid) as e: 694 | await app.edit_message_text(sender, edit_id, "❌ Access denied. Have you joined the channel?") 695 | except Exception as e: 696 | print(f"Error in message handling: {e}") 697 | await app.send_message(LOG_GROUP, f"**Error:** {str(e)}") 698 | finally: 699 | # Cleanup 700 | if file_path: 701 | await self.file_ops._cleanup_file(file_path) 702 | gc.collect() 703 | 704 | async def _parse_message_link(self, msg_link: str, offset: int, protected_channels: Set[int], sender: int, edit_id: int) -> Tuple[Optional[int], Optional[int]]: 705 | """Parse different types of message links""" 706 | if 't.me/c/' in msg_link or 't.me/b/' in msg_link: 707 | parts = msg_link.split("/") 708 | if 't.me/b/' in msg_link: 709 | chat_id = parts[-2] 710 | msg_id = int(parts[-1]) + offset 711 | else: 712 | chat_id = int('-100' + parts[parts.index('c') + 1]) 713 | msg_id = int(parts[-1]) + offset 714 | 715 | if chat_id in protected_channels: 716 | await app.edit_message_text(sender, edit_id, "❌ This channel is protected by **Team SPY**.") 717 | return None, None 718 | 719 | return chat_id, msg_id 720 | 721 | elif '/s/' in msg_link: 722 | # Handle story links 723 | await app.edit_message_text(sender, edit_id, "📖 Story Link Detected...") 724 | if not gf: 725 | await app.edit_message_text(sender, edit_id, "❌ Login required to save stories...") 726 | return None, None 727 | 728 | parts = msg_link.split("/") 729 | chat = f"-100{parts[3]}" if parts[3].isdigit() else parts[3] 730 | msg_id = int(parts[-1]) 731 | await self._download_user_stories(gf, chat, msg_id, sender, edit_id) 732 | return None, None 733 | 734 | else: 735 | # Handle public links 736 | await app.edit_message_text(sender, edit_id, "🔗 Public link detected...") 737 | chat = msg_link.split("t.me/")[1].split("/")[0] 738 | msg_id = int(msg_link.split("/")[-1]) 739 | await self._copy_public_message(app, gf, sender, chat, msg_id, edit_id) 740 | return None, None 741 | 742 | async def _handle_special_messages(self, msg, target_chat_id: int, topic_id: Optional[int], edit_id: int, sender: int) -> bool: 743 | """Handle special message types that don't require downloading""" 744 | if msg.media == MessageMediaType.WEB_PAGE_PREVIEW: 745 | result = await app.send_message(target_chat_id, msg.text.markdown, reply_to_message_id=topic_id) 746 | await result.copy(LOG_GROUP) 747 | await app.delete_messages(sender, edit_id) 748 | return True 749 | 750 | if msg.text: 751 | result = await app.send_message(target_chat_id, msg.text.markdown, reply_to_message_id=topic_id) 752 | await result.copy(LOG_GROUP) 753 | await app.delete_messages(sender, edit_id) 754 | return True 755 | 756 | return False 757 | 758 | async def _handle_direct_media(self, msg, target_chat_id: int, topic_id: Optional[int], edit_id: int, media_type: str) -> bool: 759 | """Handle media that can be sent directly without downloading""" 760 | result = None 761 | 762 | try: 763 | if media_type == "sticker": 764 | result = await app.send_sticker(target_chat_id, msg.sticker.file_id, reply_to_message_id=topic_id) 765 | elif media_type == "voice": 766 | result = await app.send_voice(target_chat_id, msg.voice.file_id, reply_to_message_id=topic_id) 767 | elif media_type == "video_note": 768 | result = await app.send_video_note(target_chat_id, msg.video_note.file_id, reply_to_message_id=topic_id) 769 | 770 | if result: 771 | await result.copy(LOG_GROUP) 772 | await app.delete_messages(msg.chat.id, edit_id) 773 | return True 774 | 775 | except Exception as e: 776 | print(f"Direct media send failed: {e}") 777 | return False 778 | 779 | return False 780 | 781 | async def _download_user_stories(self, userbot, chat_id: str, msg_id: int, sender: int, edit_id: int): 782 | """Download and send user stories""" 783 | try: 784 | edit_msg = await app.edit_message_text(sender, edit_id, "📖 Downloading Story...") 785 | story = await userbot.get_stories(chat_id, msg_id) 786 | 787 | if not story or not story.media: 788 | await edit_msg.edit("❌ No story available or no media.") 789 | return 790 | 791 | file_path = await userbot.download_media(story) 792 | await edit_msg.edit("📤 Uploading Story...") 793 | 794 | if story.media == MessageMediaType.VIDEO: 795 | await app.send_video(sender, file_path) 796 | elif story.media == MessageMediaType.DOCUMENT: 797 | await app.send_document(sender, file_path) 798 | elif story.media == MessageMediaType.PHOTO: 799 | await app.send_photo(sender, file_path) 800 | 801 | if file_path and os.path.exists(file_path): 802 | os.remove(file_path) 803 | await edit_msg.edit("✅ Story processed successfully.") 804 | 805 | except RPCError as e: 806 | await app.edit_message_text(sender, edit_id, f"❌ Error: {e}") 807 | 808 | async def _copy_public_message(self, app_client, userbot, sender: int, chat_id: str, message_id: int, edit_id: int): 809 | """Handle copying from public channels/groups""" 810 | target_chat_str = self.user_chat_ids.get(sender, str(sender)) 811 | target_chat_id, topic_id = self.parse_target_chat(target_chat_str) 812 | file_path = None 813 | 814 | try: 815 | # Try direct copy first 816 | msg = await app_client.get_messages(chat_id, message_id) 817 | custom_caption = self.user_caption_prefs.get(str(sender), "") 818 | final_caption = await self._format_caption_with_custom(msg.caption or '', sender, custom_caption) 819 | 820 | if msg.media and not msg.document and not msg.video: 821 | # For photos and other simple media 822 | if msg.photo: 823 | result = await app_client.send_photo(target_chat_id, msg.photo.file_id, caption=final_caption, reply_to_message_id=topic_id) 824 | elif msg.video: 825 | result = await app_client.send_video(target_chat_id, msg.video.file_id, caption=final_caption, reply_to_message_id=topic_id) 826 | elif msg.document: 827 | result = await app_client.send_document(target_chat_id, msg.document.file_id, caption=final_caption, reply_to_message_id=topic_id) 828 | 829 | if 'result' in locals(): 830 | await result.copy(LOG_GROUP) 831 | await app.delete_messages(sender, edit_id) 832 | return 833 | 834 | elif msg.text: 835 | result = await app_client.copy_message(target_chat_id, chat_id, message_id, reply_to_message_id=topic_id) 836 | await result.copy(LOG_GROUP) 837 | await app.delete_messages(sender, edit_id) 838 | return 839 | 840 | # If direct copy failed, try with userbot 841 | if userbot: 842 | edit_msg = await app.edit_message_text(sender, edit_id, "🔄 Trying alternative method...") 843 | try: 844 | await userbot.join_chat(chat_id) 845 | except: 846 | pass 847 | 848 | chat_id = (await userbot.get_chat(f"@{chat_id}")).id 849 | msg = await userbot.get_messages(chat_id, message_id) 850 | 851 | if not msg or msg.service or msg.empty: 852 | await edit_msg.edit("❌ Message not found or inaccessible") 853 | return 854 | 855 | if msg.text: 856 | await app_client.send_message(target_chat_id, msg.text.markdown, reply_to_message_id=topic_id) 857 | await edit_msg.delete() 858 | return 859 | 860 | # Download and upload media 861 | final_caption = await self._format_caption_with_custom(msg.caption.markdown if msg.caption else "", sender, custom_caption) 862 | 863 | progress_args = ("Downloading...", edit_msg, time.time()) 864 | file_path = await userbot.download_media(msg, progress=progress_bar, progress_args=progress_args) 865 | file_path = await self.file_ops.process_filename(file_path, sender) 866 | 867 | filename, file_size, media_type = self.media_processor.get_media_info(msg) 868 | 869 | if media_type == "photo": 870 | result = await app_client.send_photo(target_chat_id, file_path, caption=final_caption, reply_to_message_id=topic_id) 871 | elif file_size > self.config.SIZE_LIMIT: 872 | free_check = 0 873 | if 'chk_user' in globals(): 874 | free_check = await chk_user(chat_id, sender) 875 | 876 | if free_check == 1 or not self.pro_client: 877 | await edit_msg.delete() 878 | await self.file_ops.split_large_file(file_path, app_client, sender, target_chat_id, final_caption, topic_id) 879 | return 880 | else: 881 | await self.handle_large_file_upload(file_path, sender, edit_msg, final_caption) 882 | return 883 | else: 884 | upload_method = self.db.get_user_data(sender, "upload_method", "Pyrogram") 885 | if upload_method == "Telethon": 886 | await self.upload_with_telethon(file_path, sender, target_chat_id, final_caption, topic_id, edit_msg) 887 | else: 888 | await self.upload_with_pyrogram(file_path, sender, target_chat_id, final_caption, topic_id, edit_msg) 889 | 890 | except Exception as e: 891 | print(f"Public message copy error: {e}") 892 | finally: 893 | if file_path: 894 | await self.file_ops._cleanup_file(file_path) 895 | 896 | async def _format_caption_with_custom(self, original_caption: str, sender: int, custom_caption: str) -> str: 897 | """Format caption with user preferences""" 898 | delete_words = set(self.db.get_user_data(sender, "delete_words", [])) 899 | replacements = self.db.get_user_data(sender, "replacement_words", {}) 900 | 901 | processed = original_caption 902 | for word in delete_words: 903 | processed = processed.replace(word, ' ') 904 | 905 | for word, replace_word in replacements.items(): 906 | processed = processed.replace(word, replace_word) 907 | 908 | if custom_caption: 909 | return f"{processed}\n\n__**{custom_caption}**__" if processed else f"__**{custom_caption}**__" 910 | return processed 911 | 912 | async def send_settings_panel(self, chat_id: int, user_id: int): 913 | """Send enhanced settings panel""" 914 | buttons = [ 915 | [Button.inline("Set Chat ID", b'setchat'), Button.inline("Set Rename Tag", b'setrename')], 916 | [Button.inline("Caption", b'setcaption'), Button.inline("Replace Words", b'setreplacement')], 917 | [Button.inline("Remove Words", b'delete'), Button.inline("Reset All", b'reset')], 918 | [Button.inline("Session Login", b'addsession'), Button.inline("Logout", b'logout')], 919 | [Button.inline("Set Thumbnail", b'setthumb'), Button.inline("Remove Thumbnail", b'remthumb')], 920 | [Button.inline("PDF Watermark", b'pdfwt'), Button.inline("Video Watermark", b'watermark')], 921 | [Button.inline("Upload Method", b'uploadmethod')], 922 | [Button.url("Report Issues", "https://t.me/team_spy_pro")] 923 | ] 924 | 925 | message = ( 926 | "🛠 **Advanced Settings Panel**\n\n" 927 | "Customize your bot experience with these options:\n" 928 | "• Configure upload methods\n" 929 | "• Set custom captions and rename tags\n" 930 | "• Manage word filters and replacements\n" 931 | "• Handle thumbnails and watermarks\n\n" 932 | "Select an option to get started!" 933 | ) 934 | 935 | await gf.send_file(chat_id, file=self.config.SETTINGS_PIC, caption=message, buttons=buttons) 936 | 937 | # Initialize the main bot instance 938 | telegram_bot = SmartTelegramBot() 939 | 940 | # Event Handlers 941 | @gf.on(events.NewMessage(incoming=True, pattern='/settings')) 942 | async def settings_command_handler(event): 943 | """Handle /settings command""" 944 | await telegram_bot.send_settings_panel(event.chat_id, event.sender_id) 945 | 946 | @gf.on(events.CallbackQuery) 947 | async def callback_query_handler(event): 948 | """Enhanced callback query handler with all features""" 949 | user_id = event.sender_id 950 | data = event.data 951 | 952 | # Upload method selection 953 | if data == b'uploadmethod': 954 | current_method = telegram_bot.db.get_user_data(user_id, "upload_method", "Pyrogram") 955 | pyro_check = " ✅" if current_method == "Pyrogram" else "" 956 | tele_check = " ✅" if current_method == "Telethon" else "" 957 | 958 | buttons = [ 959 | [Button.inline(f"Pyrogram v2{pyro_check}", b'pyrogram')], 960 | [Button.inline(f"SpyLib v1 ⚡{tele_check}", b'telethon')] 961 | ] 962 | await event.edit( 963 | "📤 **Choose Upload Method:**\n\n" 964 | "**Pyrogram v2:** Standard, reliable uploads\n" 965 | "**SpyLib v1 ⚡:** Advanced features, beta version\n\n" 966 | "**Note:** SpyLib is built on Telethon and offers enhanced capabilities.", 967 | buttons=buttons 968 | ) 969 | 970 | elif data == b'pyrogram': 971 | telegram_bot.db.save_user_data(user_id, "upload_method", "Pyrogram") 972 | await event.edit("✅ Upload method set to **Pyrogram v2**") 973 | 974 | elif data == b'telethon': 975 | telegram_bot.db.save_user_data(user_id, "upload_method", "Telethon") 976 | await event.edit("✅ Upload method set to **SpyLib v1 ⚡**\n\nThanks for helping us test this advanced library!") 977 | 978 | # Session management 979 | elif data == b'logout': 980 | await odb.remove_session(user_id) 981 | user_data = await odb.get_data(user_id) 982 | message = "✅ Logged out successfully!" if user_data and user_data.get("session") is None else "❌ You are not logged in." 983 | await event.respond(message) 984 | 985 | elif data == b'addsession': 986 | telegram_bot.user_sessions[user_id] = 'addsession' 987 | await event.respond("🔑 **Session Login**\n\nSend your Pyrogram V2 session string:") 988 | 989 | # Settings configuration 990 | elif data == b'setchat': 991 | telegram_bot.user_sessions[user_id] = 'setchat' 992 | await event.respond("💬 **Set Target Chat**\n\nSend the chat ID where files should be sent:") 993 | 994 | elif data == b'setrename': 995 | telegram_bot.user_sessions[user_id] = 'setrename' 996 | await event.respond("🏷 **Set Rename Tag**\n\nSend the tag to append to filenames:") 997 | 998 | elif data == b'setcaption': 999 | telegram_bot.user_sessions[user_id] = 'setcaption' 1000 | await event.respond("📝 **Set Custom Caption**\n\nSend the caption to add to all files:") 1001 | 1002 | elif data == b'setreplacement': 1003 | telegram_bot.user_sessions[user_id] = 'setreplacement' 1004 | await event.respond( 1005 | "🔄 **Word Replacement**\n\n" 1006 | "Send replacement rules in format:\n" 1007 | "`'OLD_WORD' 'NEW_WORD'`\n\n" 1008 | "Example: `'sample' 'example'`" 1009 | ) 1010 | 1011 | elif data == b'delete': 1012 | telegram_bot.user_sessions[user_id] = 'deleteword' 1013 | await event.respond( 1014 | "🗑 **Delete Words**\n\n" 1015 | "Send words separated by spaces to remove them from captions/filenames:" 1016 | ) 1017 | 1018 | # Thumbnail management 1019 | elif data == b'setthumb': 1020 | telegram_bot.pending_photos.add(user_id) 1021 | await event.respond("🖼 **Set Thumbnail**\n\nSend a photo to use as thumbnail for videos:") 1022 | 1023 | elif data == b'remthumb': 1024 | thumb_path = f'{user_id}.jpg' 1025 | if os.path.exists(thumb_path): 1026 | os.remove(thumb_path) 1027 | await event.respond('✅ Thumbnail removed successfully!') 1028 | else: 1029 | await event.respond("❌ No thumbnail found to remove.") 1030 | 1031 | # Watermark features (placeholder) 1032 | elif data == b'pdfwt': 1033 | await event.respond("🚧 **PDF Watermark**\n\nThis feature is under development...") 1034 | 1035 | elif data == b'watermark': 1036 | await event.respond("🚧 **Video Watermark**\n\nThis feature is under development...") 1037 | 1038 | # Reset all settings 1039 | elif data == b'reset': 1040 | try: 1041 | success = telegram_bot.db.reset_user_data(user_id) 1042 | telegram_bot.user_chat_ids.pop(user_id, None) 1043 | telegram_bot.user_rename_prefs.pop(str(user_id), None) 1044 | telegram_bot.user_caption_prefs.pop(str(user_id), None) 1045 | 1046 | # Remove thumbnail 1047 | thumb_path = f"{user_id}.jpg" 1048 | if os.path.exists(thumb_path): 1049 | os.remove(thumb_path) 1050 | 1051 | if success: 1052 | await event.respond("✅ All settings reset successfully!\n\nUse /logout to remove session.") 1053 | else: 1054 | await event.respond("❌ Error occurred while resetting settings.") 1055 | except Exception as e: 1056 | await event.respond(f"❌ Reset failed: {e}") 1057 | 1058 | @gf.on(events.NewMessage(func=lambda e: e.sender_id in telegram_bot.pending_photos)) 1059 | async def thumbnail_handler(event): 1060 | """Handle thumbnail upload""" 1061 | user_id = event.sender_id 1062 | if event.photo: 1063 | temp_path = await event.download_media() 1064 | thumb_path = f'{user_id}.jpg' 1065 | 1066 | if os.path.exists(thumb_path): 1067 | os.remove(thumb_path) 1068 | 1069 | os.rename(temp_path, f'./{user_id}.jpg') 1070 | await event.respond('✅ Thumbnail saved successfully!') 1071 | else: 1072 | await event.respond('❌ Please send a photo. Try again.') 1073 | 1074 | telegram_bot.pending_photos.discard(user_id) 1075 | 1076 | @gf.on(events.NewMessage) 1077 | async def user_input_handler(event): 1078 | """Handle user input based on current session state""" 1079 | user_id = event.sender_id 1080 | 1081 | if user_id in telegram_bot.user_sessions: 1082 | session_type = telegram_bot.user_sessions[user_id] 1083 | 1084 | if session_type == 'setchat': 1085 | try: 1086 | chat_id = event.text.strip() 1087 | telegram_bot.user_chat_ids[user_id] = chat_id 1088 | await event.respond(f"✅ Target chat set to: `{chat_id}`") 1089 | except ValueError: 1090 | await event.respond("❌ Invalid chat ID format!") 1091 | 1092 | elif session_type == 'setrename': 1093 | rename_tag = event.text.strip() 1094 | telegram_bot.user_rename_prefs[str(user_id)] = rename_tag 1095 | telegram_bot.db.save_user_data(user_id, "rename_tag", rename_tag) 1096 | await event.respond(f"✅ Rename tag set to: **{rename_tag}**") 1097 | 1098 | elif session_type == 'setcaption': 1099 | custom_caption = event.text.strip() 1100 | telegram_bot.user_caption_prefs[str(user_id)] = custom_caption 1101 | telegram_bot.db.save_user_data(user_id, "custom_caption", custom_caption) 1102 | await event.respond(f"✅ Custom caption set to:\n\n**{custom_caption}**") 1103 | 1104 | elif session_type == 'setreplacement': 1105 | match = re.match(r"'(.+)' '(.+)'", event.text) 1106 | if not match: 1107 | await event.respond("❌ **Invalid format!**\n\nUse: `'OLD_WORD' 'NEW_WORD'`") 1108 | else: 1109 | old_word, new_word = match.groups() 1110 | delete_words = set(telegram_bot.db.get_user_data(user_id, "delete_words", [])) 1111 | 1112 | if old_word in delete_words: 1113 | await event.respond(f"❌ '{old_word}' is in delete list and cannot be replaced.") 1114 | else: 1115 | replacements = telegram_bot.db.get_user_data(user_id, "replacement_words", {}) 1116 | replacements[old_word] = new_word 1117 | telegram_bot.db.save_user_data(user_id, "replacement_words", replacements) 1118 | await event.respond(f"✅ Replacement saved:\n**'{old_word}' → '{new_word}'**") 1119 | 1120 | elif session_type == 'addsession': 1121 | session_string = event.text.strip() 1122 | await odb.set_session(user_id, session_string) 1123 | await event.respond("✅ Session string added successfully!") 1124 | 1125 | elif session_type == 'deleteword': 1126 | words_to_delete = event.text.split() 1127 | delete_words = set(telegram_bot.db.get_user_data(user_id, "delete_words", [])) 1128 | delete_words.update(words_to_delete) 1129 | telegram_bot.db.save_user_data(user_id, "delete_words", list(delete_words)) 1130 | await event.respond(f"✅ Words added to delete list:\n**{', '.join(words_to_delete)}**") 1131 | 1132 | # Clear session after handling 1133 | del telegram_bot.user_sessions[user_id] 1134 | 1135 | @gf.on(events.NewMessage(incoming=True, pattern='/lock')) 1136 | async def lock_channel_handler(event): 1137 | """Handle channel locking command (owner only)""" 1138 | if event.sender_id not in OWNER_ID: 1139 | await event.respond("❌ You are not authorized to use this command.") 1140 | return 1141 | 1142 | try: 1143 | channel_id = int(event.text.split(' ')[1]) 1144 | success = telegram_bot.db.lock_channel(channel_id) 1145 | 1146 | if success: 1147 | await event.respond(f"✅ Channel ID `{channel_id}` locked successfully.") 1148 | else: 1149 | await event.respond(f"❌ Failed to lock channel ID `{channel_id}`.") 1150 | except (ValueError, IndexError): 1151 | await event.respond("❌ **Invalid command format.**\n\nUse: `/lock CHANNEL_ID`") 1152 | except Exception as e: 1153 | await event.respond(f"❌ Error: {str(e)}") 1154 | 1155 | # Main message handler function (integration point with existing get_msg function) 1156 | async def get_msg(userbot, sender, edit_id, msg_link, i, message): 1157 | """Main integration function - enhanced version of original get_msg""" 1158 | await telegram_bot.handle_message_download(userbot, sender, edit_id, msg_link, i, message) 1159 | 1160 | print("✅ Smart Telegram Bot initialized successfully!") 1161 | print(f"📊 Features loaded:") 1162 | print(f" • Database: {'✅' if telegram_bot.db else '❌'}") 1163 | print(f" • Pro Client (4GB): {'✅' if telegram_bot.pro_client else '❌'}") 1164 | print(f" • Userbot: {'✅' if gf else '❌'}") 1165 | print(f" • App Client: {'✅' if app else '❌'}") 1166 | --------------------------------------------------------------------------------