├── .gitattributes ├── .gitignore ├── Procfile ├── README.md ├── app.json ├── main ├── __init__.py ├── __main__.py ├── bot │ ├── __init__.py │ ├── clients.py │ └── plugins │ │ ├── callback.py │ │ ├── start.py │ │ └── stream.py ├── server │ ├── __init__.py │ ├── exceptions.py │ └── stream_routes.py ├── template │ ├── dl.html │ └── req.html ├── utils │ ├── Translation.py │ ├── __init__.py │ ├── config_parser.py │ ├── custom_dl.py │ ├── file_properties.py │ ├── human_readable.py │ ├── keepalive.py │ ├── render_template.py │ └── time_format.py └── vars.py ├── requirements.txt └── runtime.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | 116 | #session files 117 | *.session 118 | *.session-journal 119 | .env 120 | test.py 121 | test2.py -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python -m main -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

TG Direct Link Generator

2 |

3 | 4 | TG-Direct-Link-Generator 5 | 6 |

7 | A Telegram Bot To Get Direct Links Of Telegram Files.
8 | Demo Bot 9 |
10 |

11 |

12 | 13 |
14 | 15 |
16 | Table of Contents 17 |
    18 |
  1. 19 | About this Bot 20 | 23 |
  2. 24 |
  3. 25 | How to make your own 26 | 30 |
  4. 31 |
  5. Setting up things
  6. 32 | 36 |
  7. How to use the bot
  8. 37 |
  9. Contact me
  10. 38 |
39 |
40 | 41 | ## About This Bot 42 | 43 |

44 | 45 | Telegram Logo 46 | 47 |

48 |

49 | This bot will give you stream links for Telegram files without the need of waiting till the download completes 50 |

51 | 52 | ### Original Repository 53 | 54 | The main working part was taken from [TG-Direct-Link-Generator](https://github.com/DeekshithSH/TG-Direct-Link-Generator). 55 | 56 | ## How to make your own 57 | 58 | Either you could locally host or deploy on [Heroku](https://heroku.com) 59 | 60 | ### Deploy on Heroku 61 | 62 | Press the below button to fast deploy to Heroku 63 | 64 | - [![Deploy To Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 65 | 66 | then goto the variables tab for more info on setting up environmental variables. 67 | 68 | ### Host it on VPS or Locally 69 | 70 | ```sh 71 | git clone https://github.com/TechShreyash/TG-Direct-Link-Generator 72 | cd TG-Direct-Link-Generator 73 | virtualenv -p /usr/bin/python3 venv 74 | . ./venv/bin/activate 75 | pip install -r requirements.txt 76 | python3 -m main 77 | ``` 78 | 79 | and to stop the whole bot, 80 | do CTRL+C 81 | 82 | ## Setting up things 83 | 84 | If you're on Heroku, just add these in the Environmental Variables 85 | or if you're Locally hosting, create a file named `.env` in the root directory and add all the variables there. 86 | An example of `.env` file: 87 | 88 | ```sh 89 | API_ID=452525 90 | API_HASH=esx576f8738x883f3sfzx83 91 | BOT_TOKEN=55838383:yourtbottokenhere 92 | BIN_CHANNEL=-100 93 | MULTI_TOKEN1=55838383:yourfirstmulticlientbottokenhere 94 | MULTI_TOKEN2=55838383:yoursecondmulticlientbottokenhere 95 | MULTI_TOKEN3=55838383:yourthirdmulticlientbottokenhere 96 | PORT=8080 97 | FQDN=yourserverip 98 | HAS_SSL=False 99 | ``` 100 | 101 | ### Mandatory Vars 102 | 103 | `API_ID` : Goto [my.telegram.org](https://my.telegram.org) to obtain this. 104 | 105 | `API_HASH` : Goto [my.telegram.org](https://my.telegram.org) to obtain this. 106 | 107 | `BOT_TOKEN` : Get the bot token from [@BotFather](https://telegram.dog/BotFather) 108 | 109 | `BIN_CHANNEL` : Create a new channel (private/public), post something in your channel. Forward that post to [@missrose_bot](https://telegram.dog/MissRose_bot) and **reply** `/id`. Now copy paste the forwarded channel ID in this field. 110 | 111 | `OWNER_ID` : Your Telegram User ID 112 | 113 | ### For MultiClient 114 | 115 | `MULTI_TOKEN1`: Add your first bot token or session strings here. 116 | 117 | `MULTI_TOKEN2`: Add your second bot token or session strings here. 118 | 119 | you may also add as many as bots you want. (max limit is not tested yet) 120 | `MULTI_TOKEN3`, `MULTI_TOKEN4`, etc. 121 | 122 | 123 | 124 | ### Optional Vars 125 | 126 | `SLEEP_THRESHOLD` : Set a sleep threshold for flood wait exceptions happening globally in this telegram bot instance, below which any request that raises a flood wait will be automatically invoked again after sleeping for the required amount of time. Flood wait exceptions requiring higher waiting times will be raised. Defaults to 60 seconds. 127 | 128 | `WORKERS` : Number of maximum concurrent workers for handling incoming updates. Defaults to `3` 129 | 130 | `PORT` : The port that you want your webapp to be listened to. Defaults to `8080` 131 | 132 | `WEB_SERVER_BIND_ADDRESS` : Your server bind address. Defauls to `0.0.0.0` 133 | 134 | `NO_PORT` : (can be either `True` or `False`) If you don't want your port to be displayed. You should point your `PORT` to `80` (http) or `443` (https) for the links to work. Ignore this if you're on Heroku. 135 | 136 | `FQDN` : A Fully Qualified Domain Name if present. Defaults to `WEB_SERVER_BIND_ADDRESS` 137 | 138 | `HAS_SSL` : (can be either `True` or `False`) If you want the generated links in https format. 139 | 140 | `PING_INTERVAL` : The time in ms you want the servers to be pinged each time to avoid sleeping (Only for Heroku). Defaults to `1200` or 20 minutes. 141 | 142 | 143 | 144 | ## How to use the bot 145 | 146 | :warning: **Before using the bot, don't forget to add all the bots (multi-client ones too) to the `BIN_CHANNEL` as an admin** 147 | 148 | `/start` : To check if the bot is alive or not. 149 | 150 | To get an instant stream link, just forward any media to the bot and boom, its fast af. 151 | 152 | ## FAQ 153 | 154 | - How long the links will remain valid or is there any expiration time for the links generated by the bot? 155 | > The links will will be valid as longs as your bot is alive and you haven't deleted the log channel. 156 | 157 | ## Contact me 158 | 159 | [![Telegram Channel](https://img.shields.io/static/v1?label=Join&message=Telegram%20Channel&color=blueviolet&style=for-the-badge&logo=telegram&logoColor=violet)](https://telegram.me/TechZBots) 160 | [![Telegram Group](https://img.shields.io/static/v1?label=Join&message=Telegram%20Group&color=blueviolet&style=for-the-badge&logo=telegram&logoColor=violet)](https://telegram.me/TechZBots_Support) 161 | 162 | You can contact either via my [Telegram Group](https://telegram.me/TechZBots_Support) or you can PM me on [@Tech_Shreyash](https://telegram.me/Tech_Shreyash) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TG Direct Link Generator", 3 | "description": "A Telegram Bot To Get Direct Links Of Telegram Files.", 4 | "logo": "https://telegra.ph/file/4d124400b985b2fe6ee1c.jpg", 5 | "keywords": [ 6 | "telegram", 7 | "pyrogram", 8 | "aiohttp", 9 | "python", 10 | "techzbots" 11 | ], 12 | "repository": "https://github.com/TechShreyash/TG-Direct-Link-Generator", 13 | "env": { 14 | "APP_NAME": { 15 | "description": "Copy-Paste the app name that you just typed above." 16 | }, 17 | "API_ID": { 18 | "description": "Get this value from https://my.telegram.org" 19 | }, 20 | "API_HASH": { 21 | "description": "Get this value from https://my.telegram.org" 22 | }, 23 | "BOT_TOKEN": { 24 | "description": "Get this value from @BotFather" 25 | }, 26 | "BIN_CHANNEL": { 27 | "description": "The BIN Channel ID. Read the readme for more info about this var" 28 | }, 29 | "WORKERS": { 30 | "description": "Number of workers that is to be assigned. Read the readme for more info about this var", 31 | "required": false 32 | }, 33 | "PING_INTERVAL": { 34 | "description": "The time in ms you want the servers to be pinged each time to avoid sleeping.", 35 | "required": false 36 | }, 37 | "OWNER_ID": { 38 | "description": "Your Telegram User ID" 39 | } 40 | }, 41 | "buildpacks": [{ 42 | "url": "heroku/python" 43 | }], 44 | "formation": { 45 | "web": { 46 | "quantity": 1, 47 | "size": "free" 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /main/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | 4 | import time 5 | from .vars import Var 6 | from main.bot.clients import StreamBot 7 | 8 | __version__ = 2.2 9 | StartTime = time.time() 10 | -------------------------------------------------------------------------------- /main/__main__.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | import sys 4 | import asyncio 5 | import logging 6 | from .vars import Var 7 | from aiohttp import web 8 | from pyrogram import idle 9 | from main import utils 10 | from main import StreamBot 11 | from main.server import web_server 12 | from main.bot.clients import initialize_clients 13 | 14 | 15 | logging.basicConfig( 16 | level=logging.INFO, 17 | datefmt="%d/%m/%Y %H:%M:%S", 18 | format='[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s', 19 | handlers=[logging.StreamHandler(stream=sys.stdout), 20 | logging.FileHandler("streambot.log", mode="a", encoding="utf-8")],) 21 | 22 | logging.getLogger("aiohttp").setLevel(logging.ERROR) 23 | logging.getLogger("pyrogram").setLevel(logging.ERROR) 24 | logging.getLogger("aiohttp.web").setLevel(logging.ERROR) 25 | 26 | server = web.AppRunner(web_server()) 27 | 28 | if sys.version_info[1] > 9: 29 | loop = asyncio.new_event_loop() 30 | asyncio.set_event_loop(loop) 31 | else: 32 | loop = asyncio.get_event_loop() 33 | 34 | async def start_services(): 35 | print() 36 | print("-------------------- Initializing Telegram Bot --------------------") 37 | await StreamBot.start() 38 | bot_info = await StreamBot.get_me() 39 | StreamBot.username = bot_info.username 40 | print("------------------------------ DONE ------------------------------") 41 | print() 42 | print( 43 | "---------------------- Initializing Clients ----------------------" 44 | ) 45 | await initialize_clients() 46 | print("------------------------------ DONE ------------------------------") 47 | if Var.ON_HEROKU: 48 | print("------------------ Starting Keep Alive Service ------------------") 49 | print() 50 | asyncio.create_task(utils.ping_server()) 51 | print("--------------------- Initalizing Web Server ---------------------") 52 | await server.setup() 53 | bind_address = "0.0.0.0" if Var.ON_HEROKU else Var.BIND_ADDRESS 54 | await web.TCPSite(server, bind_address, Var.PORT).start() 55 | print("------------------------------ DONE ------------------------------") 56 | print() 57 | print("------------------------- Service Started -------------------------") 58 | print(" bot =>> {}".format(bot_info.first_name)) 59 | if bot_info.dc_id: 60 | print(" DC ID =>> {}".format(str(bot_info.dc_id))) 61 | print(" server ip =>> {}".format(bind_address, Var.PORT)) 62 | if Var.ON_HEROKU: 63 | print(" app running on =>> {}".format(Var.FQDN)) 64 | print("------------------------------------------------------------------") 65 | print() 66 | print(""" 67 | _____________________________________________ 68 | | | 69 | | Deployed Successfully | 70 | | Join @TechZBots | 71 | |_____________________________________________| 72 | """) 73 | await idle() 74 | 75 | async def cleanup(): 76 | await server.cleanup() 77 | await StreamBot.stop() 78 | 79 | if __name__ == "__main__": 80 | try: 81 | loop.run_until_complete(start_services()) 82 | except KeyboardInterrupt: 83 | pass 84 | except Exception as err: 85 | logging.error(err.with_traceback(None)) 86 | finally: 87 | loop.run_until_complete(cleanup()) 88 | loop.stop() 89 | print("------------------------ Stopped Services ------------------------") -------------------------------------------------------------------------------- /main/bot/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | 4 | from ..vars import Var 5 | from pyrogram import Client 6 | from os import getcwd 7 | 8 | StreamBot = Client( 9 | session_name="main", 10 | api_id=Var.API_ID, 11 | api_hash=Var.API_HASH, 12 | workdir="main", 13 | plugins={"root": "main/bot/plugins"}, 14 | bot_token=Var.BOT_TOKEN, 15 | sleep_threshold=Var.SLEEP_THRESHOLD, 16 | workers=Var.WORKERS, 17 | ) 18 | 19 | multi_clients = {} 20 | work_loads = {} 21 | -------------------------------------------------------------------------------- /main/bot/clients.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | import asyncio 4 | import logging 5 | from ..vars import Var 6 | from pyrogram import Client 7 | from main.utils import TokenParser 8 | from . import multi_clients, work_loads, StreamBot 9 | 10 | 11 | async def initialize_clients(): 12 | SESSION_STRING_SIZE = 351 13 | multi_clients[0] = StreamBot 14 | work_loads[0] = 0 15 | all_tokens = TokenParser().parse_from_env() 16 | if not all_tokens: 17 | print("No additional clients found, using default client") 18 | return 19 | 20 | async def start_client(client_id, token): 21 | try: 22 | if len(token) >= 351: 23 | session_name=token 24 | bot_token=None 25 | print(f"Starting - Client {client_id} using Session Strings") 26 | else: 27 | session_name=":memory:" 28 | bot_token=token 29 | print(f"Starting - Client {client_id} using Bot Token") 30 | if client_id == len(all_tokens): 31 | await asyncio.sleep(2) 32 | print("This will take some time, please wait...") 33 | client = await Client( 34 | session_name=session_name, 35 | api_id=Var.API_ID, 36 | api_hash=Var.API_HASH, 37 | bot_token=bot_token, 38 | sleep_threshold=Var.SLEEP_THRESHOLD, 39 | no_updates=True, 40 | ).start() 41 | work_loads[client_id] = 0 42 | return client_id, client 43 | except Exception: 44 | logging.error(f"Failed starting Client - {client_id} Error:", exc_info=True) 45 | 46 | clients = await asyncio.gather(*[start_client(i, token) for i, token in all_tokens.items()]) 47 | multi_clients.update(dict(clients)) 48 | if len(multi_clients) != 1: 49 | Var.MULTI_CLIENT = True 50 | print("Multi-Client Mode Enabled") 51 | else: 52 | print("No additional clients were initialized, using default client") 53 | -------------------------------------------------------------------------------- /main/bot/plugins/callback.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | import random 4 | from main.bot import StreamBot 5 | from main.utils.file_properties import gen_link, get_media_file_unique_id 6 | from main.vars import Var 7 | from main.utils.Translation import Language, BUTTON 8 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery 9 | from pyrogram.errors import MessageDeleteForbidden 10 | 11 | 12 | deldbtnmsg=["Your Already Deleted the Link", "You can't undo the Action", "You can Resend the File to Regenerate New Link", "Why Clicking me Your Link is Dead", "This is Just a Button Showing that Your Link is Deleted"] 13 | 14 | @StreamBot.on_callback_query() 15 | async def cb_data(bot, update: CallbackQuery): 16 | # lang = getattr(Language, update.from_user.language_code) 17 | lang = getattr(Language, "en") 18 | if update.data == "home": 19 | await update.message.edit_text( 20 | text=lang.START_TEXT.format(update.from_user.mention), 21 | disable_web_page_preview=True, 22 | reply_markup=BUTTON.START_BUTTONS 23 | ) 24 | elif update.data == "help": 25 | await update.message.edit_text( 26 | text=lang.HELP_TEXT.format(Var.UPDATES_CHANNEL), 27 | disable_web_page_preview=True, 28 | reply_markup=BUTTON.HELP_BUTTONS 29 | ) 30 | elif update.data == "about": 31 | await update.message.edit_text( 32 | text=lang.ABOUT_TEXT, 33 | disable_web_page_preview=True, 34 | reply_markup=BUTTON.ABOUT_BUTTONS 35 | ) 36 | elif update.data == "close": 37 | await update.message.delete() 38 | elif update.data == "msgdeleted": 39 | await update.answer(random.choice(deldbtnmsg), show_alert=True) 40 | else: 41 | usr_cmd = update.data.split("_") 42 | if usr_cmd[0] == "msgdelconf2": 43 | await update.message.edit_text( 44 | text=update.message.text, 45 | disable_web_page_preview=True, 46 | reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("✔️", callback_data=f"msgdelyes_{usr_cmd[1]}_{usr_cmd[2]}"), InlineKeyboardButton("✖️", callback_data=f"msgdelno_{usr_cmd[1]}_{usr_cmd[2]}")]]) 47 | ) 48 | elif usr_cmd[0] == "msgdelno": 49 | get_msg = await bot.get_messages(chat_id=Var.BIN_CHANNEL, message_ids=int(usr_cmd[1])) 50 | if get_media_file_unique_id(get_msg) == usr_cmd[2]: 51 | reply_markup, Stream_Text, stream_link = await gen_link(m=update, log_msg=get_msg, from_channel=False) 52 | 53 | await update.message.edit_text( 54 | text=Stream_Text, 55 | disable_web_page_preview=True, 56 | reply_markup=reply_markup 57 | ) 58 | else: 59 | await update.answer("Message id and file_unique_id miss match", show_alert=True) 60 | elif usr_cmd[0] == "msgdelyes": 61 | try: 62 | resp = await bot.get_messages(Var.BIN_CHANNEL, int(usr_cmd[1])) 63 | if get_media_file_unique_id(resp) == usr_cmd[2]: 64 | await bot.delete_messages( 65 | chat_id=Var.BIN_CHANNEL, 66 | message_ids=int(usr_cmd[1]) 67 | ) 68 | await update.message.edit_text( 69 | text=update.message.text, 70 | disable_web_page_preview=True, 71 | reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Link Deleted", callback_data="msgdeleted")]]) 72 | ) 73 | elif resp.empty: 74 | await update.answer("Sorry Your File is Missing from the Server", show_alert=True) 75 | else: 76 | await update.answer("Message id and file_unique_id miss match", show_alert=True) 77 | except MessageDeleteForbidden as e: 78 | print(e) 79 | await bot.send_message( 80 | chat_id=Var.BIN_CHANNEL, 81 | text=f"**#ᴇʀʀᴏʀ_ᴛʀᴀᴄᴇʙᴀᴄᴋ:** `{e}`\n#Delete_Link", disable_web_page_preview=True, 82 | ) 83 | await update.answer(text='Message too old', show_alert=True) 84 | except Exception as e: 85 | print(e) 86 | error_id=await bot.send_message( 87 | chat_id=Var.BIN_CHANNEL, 88 | text=f"**#ᴇʀʀᴏʀ_ᴛʀᴀᴄᴇʙᴀᴄᴋ:** `{e}`\n#Delete_Link", disable_web_page_preview=True, 89 | ) 90 | await update.message.reply_text( 91 | text=f"**#ᴇʀʀᴏʀ_ᴛʀᴀᴄᴇʙᴀᴄᴋ:** `message-id={error_id.message_id}`\nYou can get Help from [TechZ Bots Support](https://t.me/TechZBots_Support)", disable_web_page_preview=True, 92 | ) 93 | else: 94 | await update.message.delete() -------------------------------------------------------------------------------- /main/bot/plugins/start.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | from main.bot import StreamBot 4 | from main.vars import Var 5 | from pyrogram import filters 6 | from main.utils.Translation import Language, BUTTON 7 | 8 | @StreamBot.on_message(~filters.user(Var.BANNED_USERS) & filters.command('start') & ~filters.edited) 9 | async def start(b, m): 10 | # lang = getattr(Language, m.from_user.language_code) 11 | lang = getattr(Language, "en") 12 | await m.reply_text( 13 | text=lang.START_TEXT.format(m.from_user.mention), 14 | disable_web_page_preview=True, 15 | reply_markup=BUTTON.START_BUTTONS 16 | ) 17 | 18 | 19 | @StreamBot.on_message(~filters.user(Var.BANNED_USERS) & filters.command(["about"]) & ~filters.edited) 20 | async def start(bot, update): 21 | # lang = getattr(Language, update.from_user.language_code) 22 | lang = getattr(Language, "en") 23 | await update.reply_text( 24 | text=lang.ABOUT_TEXT.format(update.from_user.mention), 25 | disable_web_page_preview=True, 26 | reply_markup=BUTTON.ABOUT_BUTTONS 27 | ) 28 | 29 | 30 | @StreamBot.on_message((filters.command('help')) & ~filters.edited & ~filters.user(Var.BANNED_USERS)) 31 | async def help_handler(bot, message): 32 | # lang = getattr(Language, message.from_user.language_code) 33 | lang = getattr(Language, "en") 34 | await message.reply_text( 35 | text=lang.HELP_TEXT.format(Var.UPDATES_CHANNEL), 36 | disable_web_page_preview=True, 37 | reply_markup=BUTTON.HELP_BUTTONS 38 | ) 39 | -------------------------------------------------------------------------------- /main/bot/plugins/stream.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | 4 | import asyncio 5 | from main.bot import StreamBot 6 | from main.utils.file_properties import gen_link 7 | from main.vars import Var 8 | from pyrogram import filters, Client 9 | from pyrogram.errors import FloodWait 10 | from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton 11 | 12 | @StreamBot.on_message( 13 | filters.private 14 | & ~filters.user(Var.BANNED_USERS) & ( 15 | filters.document 16 | | filters.video 17 | | filters.audio 18 | | filters.animation 19 | | filters.voice 20 | | filters.video_note 21 | | filters.photo 22 | | filters.sticker 23 | ), 24 | group=4, 25 | ) 26 | async def private_receive_handler(c: Client, m: Message): 27 | try: 28 | log_msg = await m.forward(chat_id=Var.BIN_CHANNEL) 29 | reply_markup, Stream_Text, stream_link = await gen_link(m=m, log_msg=log_msg, from_channel=False) 30 | await log_msg.reply_text(text=f"**Requested By :** [{m.from_user.first_name}](tg://user?id={m.from_user.id})\n**User ID :** `{m.from_user.id}`\n**Download Link :** {stream_link}", disable_web_page_preview=True, quote=True) 31 | 32 | await m.reply_text( 33 | text=Stream_Text, 34 | disable_web_page_preview=True, 35 | reply_markup=reply_markup, 36 | quote=True 37 | ) 38 | except FloodWait as e: 39 | print(f"Sleeping for {str(e.x)}s") 40 | await asyncio.sleep(e.x) 41 | await c.send_message(chat_id=Var.BIN_CHANNEL, text=f"Got Floodwait Of {str(e.x)}s from [{m.from_user.first_name}](tg://user?id={m.from_user.id})\n\n**User ID :** `{str(m.from_user.id)}`", disable_web_page_preview=True,) 42 | 43 | @StreamBot.on_message(filters.channel & ~filters.user(Var.BANNED_USERS) & (filters.document | filters.video) & ~filters.edited, group=-1) 44 | async def channel_receive_handler(bot, broadcast: Message): 45 | if int(broadcast.chat.id) in Var.BANNED_CHANNELS: 46 | await bot.leave_chat(broadcast.chat.id) 47 | return 48 | try: 49 | log_msg = await broadcast.forward(chat_id=Var.BIN_CHANNEL) 50 | stream_link = "https://{}/{}".format(Var.FQDN, log_msg.message_id) if Var.ON_HEROKU or Var.NO_PORT else \ 51 | "http://{}:{}/{}".format(Var.FQDN, 52 | Var.PORT, 53 | log_msg.message_id) 54 | await log_msg.reply_text( 55 | text=f"**Channel Name:** `{broadcast.chat.title}`\n**Channel ID:** `{broadcast.chat.id}`\n**Request URL:** https://t.me/{(await bot.get_me()).username}?start=msgid_{str(log_msg.message_id)}", 56 | # text=f"**Cʜᴀɴɴᴇʟ Nᴀᴍᴇ:** `{broadcast.chat.title}`\n**Cʜᴀɴɴᴇʟ ID:** `{broadcast.chat.id}`\n**Rᴇǫᴜᴇsᴛ ᴜʀʟ:** https://t.me/FxStreamBot?start=msgid_{str(log_msg.message_id)}", 57 | quote=True, 58 | ) 59 | await bot.edit_message_reply_markup( 60 | chat_id=broadcast.chat.id, 61 | message_id=broadcast.message_id, 62 | reply_markup=InlineKeyboardMarkup( 63 | [[InlineKeyboardButton("Download Link 📥", url=stream_link)]]) 64 | ) 65 | except FloodWait as w: 66 | print(f"Sleeping for {str(w.x)}s") 67 | await asyncio.sleep(w.x) 68 | await bot.send_message(chat_id=Var.BIN_CHANNEL, 69 | text=f"Got Floodwait Of {str(w.x)}s from {broadcast.chat.title}\n\n**Channel ID:** `{str(broadcast.chat.id)}`", 70 | disable_web_page_preview=True,) 71 | except Exception as e: 72 | await bot.send_message(chat_id=Var.BIN_CHANNEL, text=f"**#ᴇʀʀᴏʀ_ᴛʀᴀᴄᴇʙᴀᴄᴋ:** `{e}`", disable_web_page_preview=True) 73 | print(f"Can't Edit Broadcast Message!\nEʀʀᴏʀ: {e}") 74 | 75 | # Feature is Dead no New Update for Stream Link on Group 76 | @StreamBot.on_message(filters.group & ~filters.user(Var.BANNED_USERS) & (filters.document | filters.video | filters.audio) & ~filters.edited, group=4) 77 | async def private_receive_handler(c: Client, m: Message): 78 | try: 79 | log_msg = await m.forward(chat_id=Var.BIN_CHANNEL) 80 | reply_markup, Stream_Text, stream_link = await gen_link(m=m, log_msg=log_msg, from_channel=True) 81 | await log_msg.reply_text(text=f"**Requested By :** [{m.chat.first_name}](tg://user?id={m.chat.id})\n**Group ID :** `{m.from_user.id}`\n**Download Link :** {stream_link}", disable_web_page_preview=True, quote=True) 82 | 83 | await m.reply_text( 84 | text=Stream_Text, 85 | disable_web_page_preview=True, 86 | reply_markup=reply_markup, 87 | quote=True 88 | ) 89 | except FloodWait as e: 90 | print(f"Sleeping for {str(e.x)}s") 91 | await asyncio.sleep(e.x) 92 | await c.send_message(chat_id=Var.BIN_CHANNEL, text=f"Got Floodwait Of {str(e.x)}s from [{m.from_user.first_name}](tg://user?id={m.from_user.id})\n\n**User ID :** `{str(m.from_user.id)}`", disable_web_page_preview=True, ) 93 | 94 | -------------------------------------------------------------------------------- /main/server/__init__.py: -------------------------------------------------------------------------------- 1 | # Taken from megadlbot_oss 2 | # Thanks to Eyaadh 3 | # This file is a part of TG-Direct-Link-Generator 4 | 5 | from aiohttp import web 6 | from .stream_routes import routes 7 | 8 | 9 | def web_server(): 10 | web_app = web.Application(client_max_size=30000000) 11 | web_app.add_routes(routes) 12 | return web_app 13 | -------------------------------------------------------------------------------- /main/server/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class InvalidHash(Exception): 3 | message = "Invalid hash" 4 | 5 | class FIleNotFound(Exception): 6 | message = "File not found" -------------------------------------------------------------------------------- /main/server/stream_routes.py: -------------------------------------------------------------------------------- 1 | # Taken from megadlbot_oss 2 | # Thanks to Eyaadh 3 | 4 | import re 5 | import time 6 | import math 7 | import logging 8 | import secrets 9 | import mimetypes 10 | from aiohttp import web 11 | from aiohttp.http_exceptions import BadStatusLine 12 | from main.bot import multi_clients, work_loads 13 | from main.server.exceptions import FIleNotFound, InvalidHash 14 | from main import Var, utils, StartTime, __version__, StreamBot 15 | from main.utils.render_template import render_page 16 | 17 | 18 | routes = web.RouteTableDef() 19 | 20 | @routes.get("/", allow_head=True) 21 | async def root_route_handler(_): 22 | return web.json_response( 23 | { 24 | "server_status": "running", 25 | "uptime": utils.get_readable_time(time.time() - StartTime), 26 | "telegram_bot": "@" + StreamBot.username, 27 | "connected_bots": len(multi_clients), 28 | "loads": dict( 29 | ("bot" + str(c + 1), l) 30 | for c, (_, l) in enumerate( 31 | sorted(work_loads.items(), key=lambda x: x[1], reverse=True) 32 | ) 33 | ), 34 | "version": __version__, 35 | } 36 | ) 37 | 38 | @routes.get(r"/watch/{path:\S+}", allow_head=True) 39 | async def stream_handler(request: web.Request): 40 | try: 41 | path = request.match_info["path"] 42 | match = re.search(r"^([a-zA-Z0-9_-]{6})(\d+)$", path) 43 | if match: 44 | secure_hash = match.group(1) 45 | message_id = int(match.group(2)) 46 | else: 47 | message_id = int(re.search(r"(\d+)(?:\/\S+)?", path).group(1)) 48 | secure_hash = request.rel_url.query.get("hash") 49 | return web.Response(text=await render_page(message_id, secure_hash), content_type='text/html') 50 | except InvalidHash as e: 51 | raise web.HTTPForbidden(text=e.message) 52 | except FIleNotFound as e: 53 | raise web.HTTPNotFound(text=e.message) 54 | except (AttributeError, BadStatusLine, ConnectionResetError): 55 | pass 56 | except Exception as e: 57 | logging.critical(e.with_traceback(None)) 58 | raise web.HTTPInternalServerError(text=str(e)) 59 | 60 | @routes.get(r"/{path:\S+}", allow_head=True) 61 | async def stream_handler(request: web.Request): 62 | try: 63 | path = request.match_info["path"] 64 | match = re.search(r"^([a-zA-Z0-9_-]{6})(\d+)$", path) 65 | if match: 66 | secure_hash = match.group(1) 67 | message_id = int(match.group(2)) 68 | else: 69 | message_id = int(re.search(r"(\d+)(?:\/\S+)?", path).group(1)) 70 | secure_hash = request.rel_url.query.get("hash") 71 | return await media_streamer(request, message_id, secure_hash) 72 | except InvalidHash as e: 73 | raise web.HTTPForbidden(text=e.message) 74 | except FIleNotFound as e: 75 | raise web.HTTPNotFound(text=e.message) 76 | except (AttributeError, BadStatusLine, ConnectionResetError): 77 | pass 78 | except Exception as e: 79 | logging.critical(e.with_traceback(None)) 80 | raise web.HTTPInternalServerError(text=str(e)) 81 | 82 | class_cache = {} 83 | 84 | async def media_streamer(request: web.Request, message_id: int, secure_hash: str): 85 | range_header = request.headers.get("Range", 0) 86 | 87 | index = min(work_loads, key=work_loads.get) 88 | faster_client = multi_clients[index] 89 | 90 | if Var.MULTI_CLIENT: 91 | logging.info(f"Client {index} is now serving {request.remote}") 92 | 93 | if faster_client in class_cache: 94 | tg_connect = class_cache[faster_client] 95 | logging.debug(f"Using cached ByteStreamer object for client {index}") 96 | else: 97 | logging.debug(f"Creating new ByteStreamer object for client {index}") 98 | tg_connect = utils.ByteStreamer(faster_client) 99 | class_cache[faster_client] = tg_connect 100 | logging.debug("before calling get_file_properties") 101 | file_id = await tg_connect.get_file_properties(message_id) 102 | logging.debug("after calling get_file_properties") 103 | 104 | if file_id.unique_id[:6] != secure_hash: 105 | logging.debug(f"Invalid hash for message with ID {message_id}") 106 | raise InvalidHash 107 | 108 | file_size = file_id.file_size 109 | 110 | if range_header: 111 | from_bytes, until_bytes = range_header.replace("bytes=", "").split("-") 112 | from_bytes = int(from_bytes) 113 | until_bytes = int(until_bytes) if until_bytes else file_size - 1 114 | else: 115 | from_bytes = request.http_range.start or 0 116 | until_bytes = request.http_range.stop or file_size - 1 117 | 118 | req_length = until_bytes - from_bytes 119 | new_chunk_size = await utils.chunk_size(req_length) 120 | offset = await utils.offset_fix(from_bytes, new_chunk_size) 121 | first_part_cut = from_bytes - offset 122 | last_part_cut = (until_bytes % new_chunk_size) + 1 123 | part_count = math.ceil(req_length / new_chunk_size) 124 | body = tg_connect.yield_file( 125 | file_id, index, offset, first_part_cut, last_part_cut, part_count, new_chunk_size 126 | ) 127 | 128 | mime_type = file_id.mime_type 129 | file_name = file_id.file_name 130 | disposition = "attachment" 131 | if mime_type: 132 | if not file_name: 133 | try: 134 | file_name = f"{secrets.token_hex(2)}.{mime_type.split('/')[1]}" 135 | except (IndexError, AttributeError): 136 | file_name = f"{secrets.token_hex(2)}.unknown" 137 | else: 138 | if file_name: 139 | mime_type = mimetypes.guess_type(file_id.file_name) 140 | else: 141 | mime_type = "application/octet-stream" 142 | file_name = f"{secrets.token_hex(2)}.unknown" 143 | if "video/" in mime_type or "audio/" in mime_type: 144 | disposition = "inline" 145 | return_resp = web.Response( 146 | status=206 if range_header else 200, 147 | body=body, 148 | headers={ 149 | "Content-Type": f"{mime_type}", 150 | "Range": f"bytes={from_bytes}-{until_bytes}", 151 | "Content-Range": f"bytes {from_bytes}-{until_bytes}/{file_size}", 152 | "Content-Disposition": f'{disposition}; filename="{file_name}"', 153 | "Accept-Ranges": "bytes", 154 | }, 155 | ) 156 | 157 | if return_resp.status == 200: 158 | return_resp.headers.add("Content-Length", str(file_size)) 159 | 160 | return return_resp 161 | -------------------------------------------------------------------------------- /main/template/dl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %s 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 | %s 21 |
22 |
23 | 24 | 34 | 35 | 43 | 44 | 55 | 56 | -------------------------------------------------------------------------------- /main/template/req.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %s 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | %s 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 | 37 | 38 | 39 | 40 | 70 | 71 | -------------------------------------------------------------------------------- /main/utils/Translation.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | from main.vars import Var 3 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 4 | 5 | 6 | class Language(object): 7 | class en(object): 8 | START_TEXT = """ 9 | **👋 Hᴇʏ, {}**\n 10 | I'm Telegram Files Streaming Bot As Well Direct Links Generator\n 11 | Click On Help To Get More Information\n 12 | Warning 🚸\n 13 | 🔞 Pron Contents Leads To Permanenet Ban You.""" 14 | 15 | HELP_TEXT = """🔰 **How to Use Me ?** 16 | 17 | - Send Me Any File Or Media From Telegram. 18 | - I Will Provide External Direct Download Link ! 19 | 20 | **Download Link With Fastest Speed ⚡️** 21 | 22 | Warning 🚸 23 | 🔞 Pron Contents Leads To Permanenet Ban You.\n 24 | Contact Developer Or Report Bugs : [ Click Here ]""" 25 | 26 | ABOUT_TEXT = """ 27 | ⚜ My Name : TG Direct Link Generator\n 28 | ⚜ Username : @TGDirectLinkGenBot\n 29 | 🔸Version : 1.0\n 30 | 🔹Last Updated : [ 04-Apr-22 ] 31 | """ 32 | 33 | stream_msg_text =""" 34 | **Successfully Generated Your Link !**\n 35 | 📂 File Name : {}\n 36 | 📦 File Size : {}\n 37 | 📥 Download : {}\n 38 | 🖥 Watch : {}""" 39 | 40 | ban_text="__Sᴏʀʀʏ Sɪʀ, Yᴏᴜ ᴀʀᴇ Bᴀɴɴᴇᴅ ᴛᴏ ᴜsᴇ ᴍᴇ.__\n\n**[Cᴏɴᴛᴀᴄᴛ Dᴇᴠᴇʟᴏᴘᴇʀ](https://t.me/TechZBots_Support) Tʜᴇʏ Wɪʟʟ Hᴇʟᴘ Yᴏᴜ**" 41 | 42 | # ------------------------------------------------------------------------------ 43 | 44 | class BUTTON(object): 45 | START_BUTTONS = InlineKeyboardMarkup( 46 | [[ 47 | InlineKeyboardButton('Help', callback_data='help'), 48 | InlineKeyboardButton('About', callback_data='about') 49 | ], 50 | [InlineKeyboardButton("Updates Channel", url='https://t.me/TechZBots'), 51 | InlineKeyboardButton("Repo", url='https://github.com/TechShreyash/TG-Direct-Link-Generator')] 52 | ] 53 | ) 54 | HELP_BUTTONS = InlineKeyboardMarkup( 55 | [[ 56 | InlineKeyboardButton('Home', callback_data='home'), 57 | InlineKeyboardButton('About', callback_data='about') 58 | ], 59 | [ 60 | InlineKeyboardButton('Close', callback_data='close'), 61 | ], 62 | ] 63 | ) 64 | ABOUT_BUTTONS = InlineKeyboardMarkup( 65 | [[ 66 | InlineKeyboardButton('Home', callback_data='home'), 67 | InlineKeyboardButton('Help', callback_data='help') 68 | ], 69 | [ 70 | InlineKeyboardButton('Close', callback_data='close'), 71 | ] 72 | ] 73 | ) -------------------------------------------------------------------------------- /main/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | from .keepalive import ping_server 4 | from .config_parser import TokenParser 5 | from .time_format import get_readable_time 6 | from .file_properties import get_hash, get_name 7 | from .custom_dl import ByteStreamer, offset_fix, chunk_size -------------------------------------------------------------------------------- /main/utils/config_parser.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | from os import environ 4 | from typing import Dict, Optional 5 | 6 | 7 | class TokenParser: 8 | def __init__(self, config_file: Optional[str] = None): 9 | self.tokens = {} 10 | self.config_file = config_file 11 | 12 | def parse_from_env(self) -> Dict[int, str]: 13 | self.tokens = dict( 14 | (c + 1, t) 15 | for c, (_, t) in enumerate( 16 | filter( 17 | lambda n: n[0].startswith("MULTI_TOKEN"), sorted(environ.items()) 18 | ) 19 | ) 20 | ) 21 | return self.tokens 22 | -------------------------------------------------------------------------------- /main/utils/custom_dl.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | import math 4 | import asyncio 5 | import logging 6 | from main import Var 7 | from typing import Dict, Union 8 | from main.bot import work_loads 9 | from pyrogram import Client, utils, raw 10 | from .file_properties import get_file_ids 11 | from pyrogram.session import Session, Auth 12 | from pyrogram.errors import AuthBytesInvalid 13 | from main.server.exceptions import FIleNotFound 14 | from pyrogram.file_id import FileId, FileType, ThumbnailSource 15 | 16 | 17 | async def chunk_size(length): 18 | return 2 ** max(min(math.ceil(math.log2(length / 1024)), 10), 2) * 1024 19 | 20 | 21 | async def offset_fix(offset, chunksize): 22 | offset -= offset % chunksize 23 | return offset 24 | 25 | 26 | class ByteStreamer: 27 | def __init__(self, client: Client): 28 | """A custom class that holds the cache of a specific client and class functions. 29 | attributes: 30 | client: the client that the cache is for. 31 | cached_file_ids: a dict of cached file IDs. 32 | cached_file_properties: a dict of cached file properties. 33 | 34 | functions: 35 | generate_file_properties: returns the properties for a media of a specific message contained in Tuple. 36 | generate_media_session: returns the media session for the DC that contains the media file. 37 | yield_file: yield a file from telegram servers for streaming. 38 | 39 | This is a modified version of the 40 | Thanks to Eyaadh 41 | """ 42 | self.clean_timer = 30 * 60 43 | self.client: Client = client 44 | self.cached_file_ids: Dict[int, FileId] = {} 45 | asyncio.create_task(self.clean_cache()) 46 | 47 | async def get_file_properties(self, message_id: int) -> FileId: 48 | """ 49 | Returns the properties of a media of a specific message in a FIleId class. 50 | if the properties are cached, then it'll return the cached results. 51 | or it'll generate the properties from the Message ID and cache them. 52 | """ 53 | if message_id not in self.cached_file_ids: 54 | await self.generate_file_properties(message_id) 55 | logging.debug(f"Cached file properties for message with ID {message_id}") 56 | return self.cached_file_ids[message_id] 57 | 58 | async def generate_file_properties(self, message_id: int) -> FileId: 59 | """ 60 | Generates the properties of a media file on a specific message. 61 | returns ths properties in a FIleId class. 62 | """ 63 | file_id = await get_file_ids(self.client, Var.BIN_CHANNEL, message_id) 64 | logging.debug(f"Generated file ID and Unique ID for message with ID {message_id}") 65 | if not file_id: 66 | logging.debug(f"Message with ID {message_id} not found") 67 | raise FIleNotFound 68 | self.cached_file_ids[message_id] = file_id 69 | logging.debug(f"Cached media message with ID {message_id}") 70 | return self.cached_file_ids[message_id] 71 | 72 | async def generate_media_session(self, client: Client, file_id: FileId) -> Session: 73 | """ 74 | Generates the media session for the DC that contains the media file. 75 | This is required for getting the bytes from Telegram servers. 76 | """ 77 | 78 | media_session = client.media_sessions.get(file_id.dc_id, None) 79 | 80 | if media_session is None: 81 | if file_id.dc_id != await client.storage.dc_id(): 82 | media_session = Session( 83 | client, 84 | file_id.dc_id, 85 | await Auth( 86 | client, file_id.dc_id, await client.storage.test_mode() 87 | ).create(), 88 | await client.storage.test_mode(), 89 | is_media=True, 90 | ) 91 | await media_session.start() 92 | 93 | for _ in range(6): 94 | exported_auth = await client.send( 95 | raw.functions.auth.ExportAuthorization(dc_id=file_id.dc_id) 96 | ) 97 | 98 | try: 99 | await media_session.send( 100 | raw.functions.auth.ImportAuthorization( 101 | id=exported_auth.id, bytes=exported_auth.bytes 102 | ) 103 | ) 104 | break 105 | except AuthBytesInvalid: 106 | logging.debug( 107 | f"Invalid authorization bytes for DC {file_id.dc_id}" 108 | ) 109 | continue 110 | else: 111 | await media_session.stop() 112 | raise AuthBytesInvalid 113 | else: 114 | media_session = Session( 115 | client, 116 | file_id.dc_id, 117 | await client.storage.auth_key(), 118 | await client.storage.test_mode(), 119 | is_media=True, 120 | ) 121 | await media_session.start() 122 | logging.debug(f"Created media session for DC {file_id.dc_id}") 123 | client.media_sessions[file_id.dc_id] = media_session 124 | else: 125 | logging.debug(f"Using cached media session for DC {file_id.dc_id}") 126 | return media_session 127 | 128 | 129 | @staticmethod 130 | async def get_location(file_id: FileId) -> Union[raw.types.InputPhotoFileLocation, 131 | raw.types.InputDocumentFileLocation, 132 | raw.types.InputPeerPhotoFileLocation,]: 133 | """ 134 | Returns the file location for the media file. 135 | """ 136 | file_type = file_id.file_type 137 | 138 | if file_type == FileType.CHAT_PHOTO: 139 | if file_id.chat_id > 0: 140 | peer = raw.types.InputPeerUser( 141 | user_id=file_id.chat_id, access_hash=file_id.chat_access_hash 142 | ) 143 | else: 144 | if file_id.chat_access_hash == 0: 145 | peer = raw.types.InputPeerChat(chat_id=-file_id.chat_id) 146 | else: 147 | peer = raw.types.InputPeerChannel( 148 | channel_id=utils.get_channel_id(file_id.chat_id), 149 | access_hash=file_id.chat_access_hash, 150 | ) 151 | 152 | location = raw.types.InputPeerPhotoFileLocation( 153 | peer=peer, 154 | volume_id=file_id.volume_id, 155 | local_id=file_id.local_id, 156 | big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG, 157 | ) 158 | elif file_type == FileType.PHOTO: 159 | location = raw.types.InputPhotoFileLocation( 160 | id=file_id.media_id, 161 | access_hash=file_id.access_hash, 162 | file_reference=file_id.file_reference, 163 | thumb_size=file_id.thumbnail_size, 164 | ) 165 | else: 166 | location = raw.types.InputDocumentFileLocation( 167 | id=file_id.media_id, 168 | access_hash=file_id.access_hash, 169 | file_reference=file_id.file_reference, 170 | thumb_size=file_id.thumbnail_size, 171 | ) 172 | return location 173 | 174 | async def yield_file( 175 | self, 176 | file_id: FileId, 177 | index: int, 178 | offset: int, 179 | first_part_cut: int, 180 | last_part_cut: int, 181 | part_count: int, 182 | chunk_size: int, 183 | ) -> Union[str, None]: 184 | """ 185 | Custom generator that yields the bytes of the media file. 186 | Modded from 187 | Thanks to Eyaadh 188 | """ 189 | client = self.client 190 | work_loads[index] += 1 191 | logging.debug(f"Starting to yielding file with client {index}.") 192 | media_session = await self.generate_media_session(client, file_id) 193 | 194 | current_part = 1 195 | 196 | location = await self.get_location(file_id) 197 | 198 | try: 199 | r = await media_session.send( 200 | raw.functions.upload.GetFile( 201 | location=location, offset=offset, limit=chunk_size 202 | ), 203 | ) 204 | if isinstance(r, raw.types.upload.File): 205 | while current_part <= part_count: 206 | chunk = r.bytes 207 | if not chunk: 208 | break 209 | offset += chunk_size 210 | if part_count == 1: 211 | yield chunk[first_part_cut:last_part_cut] 212 | break 213 | if current_part == 1: 214 | yield chunk[first_part_cut:] 215 | if 1 < current_part <= part_count: 216 | yield chunk 217 | 218 | r = await media_session.send( 219 | raw.functions.upload.GetFile( 220 | location=location, offset=offset, limit=chunk_size 221 | ), 222 | ) 223 | 224 | current_part += 1 225 | except (TimeoutError, AttributeError): 226 | pass 227 | finally: 228 | logging.debug("Finished yielding file with {current_part} parts.") 229 | work_loads[index] -= 1 230 | 231 | 232 | async def clean_cache(self) -> None: 233 | """ 234 | function to clean the cache to reduce memory usage 235 | """ 236 | while True: 237 | await asyncio.sleep(self.clean_timer) 238 | self.cached_file_ids.clear() 239 | logging.debug("Cleaned the cache") 240 | -------------------------------------------------------------------------------- /main/utils/file_properties.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | from urllib.parse import quote_plus 4 | from pyrogram import Client 5 | from typing import Any, Optional 6 | from pyrogram.types import Message 7 | from pyrogram.file_id import FileId 8 | from pyrogram.raw.types.messages import Messages 9 | from main.server.exceptions import FIleNotFound 10 | from main.utils.Translation import Language 11 | from main.utils.human_readable import humanbytes 12 | from main.vars import Var 13 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 14 | 15 | async def parse_file_id(message: "Message") -> Optional[FileId]: 16 | media = get_media_from_message(message) 17 | if media: 18 | return FileId.decode(media.file_id) 19 | 20 | async def parse_file_unique_id(message: "Messages") -> Optional[str]: 21 | media = get_media_from_message(message) 22 | if media: 23 | return media.file_unique_id 24 | 25 | async def get_file_ids(client: Client, chat_id: int, message_id: int) -> Optional[FileId]: 26 | message = await client.get_messages(chat_id, message_id) 27 | if message.empty: 28 | raise FIleNotFound 29 | media = get_media_from_message(message) 30 | file_unique_id = await parse_file_unique_id(message) 31 | file_id = await parse_file_id(message) 32 | setattr(file_id, "file_size", getattr(media, "file_size", 0)) 33 | setattr(file_id, "mime_type", getattr(media, "mime_type", "")) 34 | setattr(file_id, "file_name", getattr(media, "file_name", "")) 35 | setattr(file_id, "unique_id", file_unique_id) 36 | return file_id 37 | 38 | def get_media_from_message(message: "Message") -> Any: 39 | media_types = ( 40 | "audio", 41 | "document", 42 | "photo", 43 | "sticker", 44 | "animation", 45 | "video", 46 | "voice", 47 | "video_note", 48 | ) 49 | for attr in media_types: 50 | media = getattr(message, attr, None) 51 | if media: 52 | return media 53 | 54 | 55 | def get_hash(media_msg: Message) -> str: 56 | media = get_media_from_message(media_msg) 57 | return getattr(media, "file_unique_id", "")[:6] 58 | 59 | def get_media_file_size(m): 60 | media = get_media_from_message(m) 61 | return getattr(media, "file_size", "None") 62 | 63 | def get_name(media_msg: Message) -> str: 64 | media = get_media_from_message(media_msg) 65 | return str(getattr(media, "file_name", "None")) 66 | 67 | def get_media_mime_type(m): 68 | media = get_media_from_message(m) 69 | return getattr(media, "mime_type", "None/unknown") 70 | 71 | def get_media_file_unique_id(m): 72 | media = get_media_from_message(m) 73 | return getattr(media, "file_unique_id", "") 74 | 75 | # Generate Text, Stream Link, reply_markup 76 | async def gen_link(m: Message,log_msg: Messages, from_channel: bool): 77 | """Generate Text for Stream Link, Reply Text and reply_markup""" 78 | # lang = getattr(Language, message.from_user.language_code) 79 | lang = getattr(Language, "en") 80 | file_name = get_name(log_msg) 81 | file_size = humanbytes(get_media_file_size(log_msg)) 82 | 83 | page_link = f"{Var.URL}watch/{get_hash(log_msg)}{log_msg.message_id}" 84 | stream_link = f"{Var.URL}{log_msg.message_id}/{quote_plus(get_name(m))}?hash={get_hash(log_msg)}" 85 | Stream_Text=lang.stream_msg_text.format(file_name, file_size, stream_link, page_link) 86 | reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("🖥STREAM", url=page_link), InlineKeyboardButton("Dᴏᴡɴʟᴏᴀᴅ 📥", url=stream_link)]]) 87 | 88 | if from_channel: 89 | reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("🖥STREAM", url=page_link), InlineKeyboardButton("Dᴏᴡɴʟᴏᴀᴅ 📥", url=stream_link)]]) 90 | else: 91 | reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("🖥STREAM", url=page_link), InlineKeyboardButton("Dᴏᴡɴʟᴏᴀᴅ 📥", url=stream_link)], 92 | [InlineKeyboardButton("❌ Delete Link", callback_data=f"msgdelconf2_{log_msg.message_id}_{get_media_file_unique_id(log_msg)}")]]) 93 | 94 | return reply_markup, Stream_Text, stream_link -------------------------------------------------------------------------------- /main/utils/human_readable.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | def humanbytes(size): 4 | # https://stackoverflow.com/a/49361727/4723940 5 | # 2**10 = 1024 6 | if not size: 7 | return "" 8 | power = 2**10 9 | n = 0 10 | Dic_powerN = {0: ' ', 1: 'Ki', 2: 'Mi', 3: 'Gi', 4: 'Ti'} 11 | while size > power: 12 | size /= power 13 | n += 1 14 | return str(round(size, 2)) + " " + Dic_powerN[n] + 'B' 15 | -------------------------------------------------------------------------------- /main/utils/keepalive.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | import asyncio 4 | import logging 5 | import aiohttp 6 | import traceback 7 | from main import Var 8 | 9 | 10 | async def ping_server(): 11 | sleep_time = Var.PING_INTERVAL 12 | while True: 13 | await asyncio.sleep(sleep_time) 14 | try: 15 | async with aiohttp.ClientSession( 16 | timeout=aiohttp.ClientTimeout(total=10) 17 | ) as session: 18 | async with session.get(Var.URL) as resp: 19 | logging.info("Pinged server with response: {}".format(resp.status)) 20 | except TimeoutError: 21 | logging.warning("Couldn't connect to the site URL..!") 22 | except Exception: 23 | traceback.print_exc() 24 | -------------------------------------------------------------------------------- /main/utils/render_template.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | from main.vars import Var 4 | from main.bot import StreamBot 5 | from main.utils.human_readable import humanbytes 6 | from main.utils.file_properties import get_file_ids 7 | from main.server.exceptions import InvalidHash 8 | import urllib.parse 9 | import aiofiles 10 | import logging 11 | import aiohttp 12 | 13 | 14 | async def render_page(message_id, secure_hash): 15 | file_data=await get_file_ids(StreamBot, int(Var.BIN_CHANNEL), int(message_id)) 16 | if file_data.unique_id[:6] != secure_hash: 17 | logging.debug(f'link hash: {secure_hash} - {file_data.unique_id[:6]}') 18 | logging.debug(f"Invalid hash for message with - ID {message_id}") 19 | raise InvalidHash 20 | src = urllib.parse.urljoin(Var.URL, f'{secure_hash}{str(message_id)}') 21 | if str(file_data.mime_type.split('/')[0].strip()) == 'video': 22 | async with aiofiles.open('main/template/req.html') as r: 23 | heading = 'Watch {}'.format(file_data.file_name) 24 | tag = file_data.mime_type.split('/')[0].strip() 25 | html = (await r.read()).replace('tag', tag) % (heading, file_data.file_name, src) 26 | elif str(file_data.mime_type.split('/')[0].strip()) == 'audio': 27 | async with aiofiles.open('main/template/req.html') as r: 28 | heading = 'Listen {}'.format(file_data.file_name) 29 | tag = file_data.mime_type.split('/')[0].strip() 30 | html = (await r.read()).replace('tag', tag) % (heading, file_data.file_name, src) 31 | else: 32 | async with aiofiles.open('main/template/dl.html') as r: 33 | async with aiohttp.ClientSession() as s: 34 | async with s.get(src) as u: 35 | heading = 'Download {}'.format(file_data.file_name) 36 | file_size = humanbytes(int(u.headers.get('Content-Length'))) 37 | html = (await r.read()) % (heading, file_data.file_name, src, file_size) 38 | return html 39 | -------------------------------------------------------------------------------- /main/utils/time_format.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | def get_readable_time(seconds: int) -> str: 4 | count = 0 5 | readable_time = "" 6 | time_list = [] 7 | time_suffix_list = ["s", "m", "h", " days"] 8 | while count < 4: 9 | count += 1 10 | if count < 3: 11 | remainder, result = divmod(seconds, 60) 12 | else: 13 | remainder, result = divmod(seconds, 24) 14 | if seconds == 0 and remainder == 0: 15 | break 16 | time_list.append(int(result)) 17 | seconds = int(remainder) 18 | for x in range(len(time_list)): 19 | time_list[x] = str(time_list[x]) + time_suffix_list[x] 20 | if len(time_list) == 4: 21 | readable_time += time_list.pop() + ", " 22 | time_list.reverse() 23 | readable_time += ": ".join(time_list) 24 | return readable_time 25 | -------------------------------------------------------------------------------- /main/vars.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-Direct-Link-Generator 2 | 3 | from os import environ 4 | from dotenv import load_dotenv 5 | 6 | load_dotenv() 7 | 8 | 9 | class Var(object): 10 | MULTI_CLIENT = False 11 | API_ID = int(environ.get("API_ID")) 12 | API_HASH = str(environ.get("API_HASH")) 13 | BOT_TOKEN = str(environ.get("BOT_TOKEN")) 14 | SLEEP_THRESHOLD = int(environ.get("SLEEP_THRESHOLD", "60")) # 1 minute 15 | WORKERS = int(environ.get("WORKERS", "6")) # 6 workers = 6 commands at once 16 | BIN_CHANNEL = int( 17 | environ.get("BIN_CHANNEL", None) 18 | ) # you NEED to use a CHANNEL when you're using MULTI_CLIENT 19 | PORT = int(environ.get("PORT", 8080)) 20 | BIND_ADDRESS = str(environ.get("WEB_SERVER_BIND_ADDRESS", "0.0.0.0")) 21 | PING_INTERVAL = int(environ.get("PING_INTERVAL", "1200")) # 20 minutes 22 | HAS_SSL = environ.get("HAS_SSL", False) 23 | HAS_SSL = True if str(HAS_SSL).lower() == "true" else False 24 | NO_PORT = environ.get("NO_PORT", False) 25 | NO_PORT = True if str(NO_PORT).lower() == "true" else False 26 | if "DYNO" in environ: 27 | ON_HEROKU = True 28 | APP_NAME = str(environ.get("APP_NAME")) 29 | else: 30 | ON_HEROKU = False 31 | FQDN = ( 32 | str(environ.get("FQDN", BIND_ADDRESS)) 33 | if not ON_HEROKU or environ.get("FQDN") 34 | else APP_NAME + ".herokuapp.com" 35 | ) 36 | if ON_HEROKU: 37 | URL = f"https://{FQDN}/" 38 | else: 39 | URL = "http{}://{}{}/".format( 40 | "s" if HAS_SSL else "", FQDN, "" if NO_PORT else ":" + str(PORT) 41 | ) 42 | 43 | UPDATES_CHANNEL = "TechZBots" 44 | OWNER_ID = int(environ.get('OWNER_ID', '777000')) 45 | 46 | BANNED_CHANNELS = list(set(int(x) for x in str(environ.get("BANNED_CHANNELS", "-1001296894100")).split())) 47 | BANNED_USERS = list(set(int(x) for x in str(environ.get("BANNED_USERS","5275470552 5287015877")).split())) 48 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | pyrogram==1.4.12 3 | python-dotenv 4 | tgcrypto 5 | aiofiles 6 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.11 2 | --------------------------------------------------------------------------------