├── .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 |
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 | -
19 | About this Bot
20 |
23 |
24 | -
25 | How to make your own
26 |
30 |
31 | - Setting up things
32 |
36 | - How to use the bot
37 | - Contact me
38 |
39 |
40 |
41 | ## About This Bot
42 |
43 |
44 |
45 |
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 | - [](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 | [](https://telegram.me/TechZBots)
160 | [](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 |
--------------------------------------------------------------------------------