├── .changelog ├── .gitattributes ├── .github └── workflows │ └── notify.yml ├── .gitignore ├── .vscode └── settings.json ├── Procfile ├── README.md ├── WebStreamer ├── __init__.py ├── __main__.py ├── clients.py ├── plugins │ ├── start.py │ └── stream.py ├── stream_routes.py ├── utils │ ├── __init__.py │ ├── file_properties.py │ ├── keepalive.py │ ├── paralleltransfer.py │ └── util.py └── vars.py ├── app.json ├── docs └── INSTALL.md └── requirements.txt /.changelog: -------------------------------------------------------------------------------- 1 | [2.21] 2 | * Changed Download Link Path 3 | * Using DataBase to Store File 4 | * Forgot f 5 | 6 | [2.30] 7 | * Fixed Error with Broadcast 8 | 9 | [2.31] 10 | * Fixed no reply for /myfiles command when user has 0 files 11 | * Removed unwanted codes and lines 12 | * Added TOS -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/notify.yml: -------------------------------------------------------------------------------- 1 | name: Notify on Telegram 2 | 3 | on: 4 | fork: 5 | push: 6 | release: 7 | types: published 8 | issue_comment: 9 | types: created 10 | watch: 11 | types: started 12 | pull_request_review_comment: 13 | types: created 14 | pull_request: 15 | types: [opened, closed, reopened] 16 | issues: 17 | types: [opened, pinned, closed, reopened] 18 | jobs: 19 | notify: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Notify the commit on Telegram. 24 | uses: EverythingSuckz/github-telegram-notify@main 25 | with: 26 | bot_token: '${{ secrets.BOT_TOKEN }}' 27 | chat_id: '${{ secrets.CHAT_ID }}' -------------------------------------------------------------------------------- /.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 | 120 | *.env 121 | test.py 122 | /test 123 | 124 | streambot.log.* 125 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "VSCord.enabled": true, 3 | "cSpell.words": [ 4 | "DYNO" 5 | ] 6 | } -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python -m WebStreamer -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ **NOTICE** 2 | > This repository has been **rewritten from scratch** and is now maintained at a new location: 3 | > 👉 [https://github.com/DeekshithSH/tg-filestream](https://github.com/DeekshithSH/tg-filestream) 4 | 5 | # Telegram File Stream Bot 6 | This bot will give you stream links for Telegram files without waiting for them to download. 7 | 8 | ### Original Repository 9 | [TG-FileStreamBot](https://github.com/DeekshithSH/TG-FileStreamBot) is a modified version of [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot) by [EverythingSuckz](https://github.com/EverythingSuckz) 10 | 11 | The main logic was taken from [Tulir Asokan](https://github.com/tulir)'s [tg filestream](https://github.com/tulir/TGFileStream) project. 12 | 13 | ## How to make your own 14 | [Click here installation page](/docs/INSTALL.md) 15 | 16 | ## Environment Variables 17 | ### 🔒 Mandatory 18 | 19 | - `API_ID` : Goto [my.telegram.org](https://my.telegram.org) to obtain this. 20 | - `API_HASH` : Goto [my.telegram.org](https://my.telegram.org) to obtain this. 21 | - `BOT_TOKEN` : Get the bot token from [@BotFather](https://telegram.dog/BotFather) 22 | - `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. 23 | 24 | ### 🧩 Optional 25 | 26 | - `ALLOWED_USERS`: A list of user IDs separated by comma (,). If this is set, only the users in this list will be able to use the bot. 27 | > **Note** 28 | > Leave this field empty and anyone will be able to use your bot instance. 29 | - `BLOCKED_USERS`: A list of user IDs separated by commas (,). If this is set, the users in this list will be prevented from using the bot. 30 | > **Note** 31 | > User IDs in this field take precedence. Even if a user is in ALLOWED_USERS, they will be blocked if they are listed here 32 | - `CACHE_SIZE` (default: 128) — Maximum number of file info entries cached per client. Each client (including those using MULTI_TOKEN) gets its own separate cache of this size 33 | - `CHUNK_SIZE`: Size of the chunk to request from Telegram server when streaming a file [See more](https://core.telegram.org/api/files#downloading-files) 34 | - `CONNECTION_LIMIT`: (default 20) - The maximum number of connections to a single Telegram datacenter. 35 | - `FQDN` : A Fully Qualified Domain Name if present. Defaults to `WEB_SERVER_BIND_ADDRESS` 36 | - `HAS_SSL` : (can be either `True` or `False`) If you want the generated links in https format. 37 | - `HASH_LENGTH`: This is the custom hash length for generated URLs. The hash length must be greater than 5 and less than 64. 38 | - `KEEP_ALIVE` : If you want to make the server ping itself every 39 | - `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. 40 | - `NO_UPDATE` if set to `true` bot won't respond to any messages 41 | - `PING_INTERVAL` : The time in seconds you want the servers to be pinged each time to avoid sleeping (Only for Heroku). Defaults to `600` or 10 minutes. 42 | - `PORT` : The port that you want your webapp to be listened to. Defaults to `8080` 43 | - `REQUEST_LIMIT`: (default 5) - The maximum number of requests a single IP can have active at a time 44 | - `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. 45 | - `TRUST_HEADERS`: (defaults to true) - Whether or not to trust X-Forwarded-For headers when logging requests. 46 | - `WEB_SERVER_BIND_ADDRESS` : Your server bind address. Defauls to `0.0.0.0` 47 | 48 | ### 🤖 Multi-Client Tokens 49 | 50 | To enable multi-client, generate new bot tokens and add it as your environmental variables with the following key names. 51 | 52 | `MULTI_TOKEN1`: Add your first bot token here. 53 | 54 | `MULTI_TOKEN2`: Add your second bot token here. 55 | 56 | you may also add as many as bots you want. (max limit is not tested yet) 57 | `MULTI_TOKEN3`, `MULTI_TOKEN4`, etc. 58 | 59 | > **Warning** 60 | > Don't forget to add all these bots to the `BIN_CHANNEL` for the proper functioning 61 | 62 | ## How to use the bot 63 | 64 | :warning: Make sure all bots are added to the `BIN_CHANNEL` as **admins**. 65 | 66 | - `/start` — Check if the bot is alive 67 | - Forward any media to get an instant stream link. 68 | 69 | ## FAQ 70 | 71 | **Q: Do the stream links expire?** 72 | A: They are valid as long as your bot is alive and the log channel isn’t deleted. 73 | 74 | ## Contributing 75 | 76 | Feel free to open issues or PRs with improvements or suggestions. 77 | 78 | ## Contact 79 | 80 | Join the [Telegram Group](https://xn--r1a.click/AWeirdString) or [Channel](https://xn--r1a.click/SpringsFern) for updates. 81 | 82 | ## Credits 83 | 84 | - [Me](https://github.com/DeekshithSH) 85 | - [Lonami](https://github.com/Lonami) for his [Telethon Library](https://github.com/LonamiWebs/Telethon) 86 | - [Tulir Asokan](https://github.com/tulir) for his [tg filestream](bit.ly/tg-stream) 87 | - [EverythingSuckz](https://github.com/EverythingSuckz) for his [TG-FileStreamBot](https://github.com/EverythingSuckz/TG-FileStreamBot) 88 | - [BlackStone](https://github.com/eyMarv) for adding multi-client support. 89 | - [eyaadh](https://github.com/eyaadh) for his awesome [Megatron Bot](https://github.com/eyaadh/megadlbot_oss). 90 | -------------------------------------------------------------------------------- /WebStreamer/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | 3 | 4 | import time 5 | 6 | __version__ = "0.1" 7 | StartTime = time.time() 8 | -------------------------------------------------------------------------------- /WebStreamer/__main__.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | 3 | import sys 4 | import asyncio 5 | import traceback 6 | import logging 7 | import logging.handlers as handlers 8 | 9 | from aiohttp import web 10 | from WebStreamer.stream_routes import routes 11 | from WebStreamer.utils.keepalive import ping_server 12 | from WebStreamer.utils.util import load_plugins, startup 13 | from WebStreamer.clients import StreamBot, initialize_clients 14 | from WebStreamer.vars import Var 15 | 16 | logging.basicConfig( 17 | level=logging.DEBUG if Var.DEBUG else logging.INFO, 18 | datefmt="%d/%m/%Y %H:%M:%S", 19 | format="[%(asctime)s][%(name)s][%(levelname)s] ==> %(message)s", 20 | handlers=[logging.StreamHandler(stream=sys.stdout), 21 | handlers.RotatingFileHandler("streambot.log", mode="a", maxBytes=104857600, backupCount=2, encoding="utf-8")],) 22 | 23 | logging.getLogger("aiohttp").setLevel(logging.DEBUG if Var.DEBUG else logging.ERROR) 24 | logging.getLogger("aiohttp.web").setLevel(logging.DEBUG if Var.DEBUG else logging.ERROR) 25 | logging.getLogger("telethon").setLevel(logging.INFO if Var.DEBUG else logging.ERROR) 26 | 27 | app = web.Application(client_max_size=1024*8) # 8KB 28 | app.add_routes(routes) 29 | server = web.AppRunner(app) 30 | 31 | async def start_services(): 32 | logging.info("Initializing Telegram Bot") 33 | await StreamBot.start(bot_token=Var.BOT_TOKEN) 34 | await startup(StreamBot) 35 | try: 36 | peer = await StreamBot.get_entity(Var.BIN_CHANNEL) 37 | except ValueError: 38 | logging.error("Bin Channel not found. Please ensure the bot has been added to the bin channel.") 39 | return 40 | bot_info = await StreamBot.get_me() 41 | Var.USERNAME = bot_info.username 42 | Var.FIRST_NAME=bot_info.first_name 43 | logging.info("Initialized Telegram Bot") 44 | logging.info("Initializing Clients") 45 | await initialize_clients() 46 | if peer.megagroup: 47 | if Var.MULTI_CLIENT: 48 | logging.error("Bin Channel is a group. It must be a channel; multi-client won't work with groups.") 49 | return 50 | else: 51 | logging.warning("Bin Channel is a group. Use a channel for multi-client support.") 52 | if not Var.NO_UPDATE: 53 | logging.info('Importing plugins') 54 | load_plugins("WebStreamer/plugins") 55 | logging.info("Imported Plugins") 56 | if Var.KEEP_ALIVE: 57 | logging.info("Starting Keep Alive Service") 58 | asyncio.create_task(ping_server()) 59 | logging.info("Initializing Web Server") 60 | await server.setup() 61 | await web.TCPSite(server, Var.BIND_ADDRESS, Var.PORT).start() 62 | logging.info("Service Started") 63 | logging.info("bot =>> %s", Var.FIRST_NAME) 64 | logging.info("DC ID =>> %s", str(StreamBot.session.dc_id)) 65 | logging.info(" URL =>> %s", Var.URL) 66 | await StreamBot.run_until_disconnected() 67 | 68 | async def main(): 69 | try: 70 | await start_services() 71 | finally: 72 | await cleanup() 73 | logging.info("Stopped Services") 74 | 75 | async def cleanup(): 76 | await server.cleanup() 77 | await StreamBot.disconnect() 78 | 79 | if __name__ == "__main__": 80 | try: 81 | asyncio.run(main()) 82 | except KeyboardInterrupt: 83 | pass 84 | except Exception: 85 | logging.error(traceback.format_exc()) 86 | -------------------------------------------------------------------------------- /WebStreamer/clients.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | # Coding : Jyothis Jayanth [@EverythingSuckz] 3 | 4 | import asyncio 5 | import logging 6 | from os import environ 7 | from telethon import TelegramClient 8 | from telethon.sessions import MemorySession 9 | from WebStreamer.utils.util import startup 10 | from WebStreamer.vars import Var 11 | 12 | multi_clients: dict[int, TelegramClient] = {} 13 | work_loads: dict[int, int] = {} 14 | 15 | StreamBot = TelegramClient( 16 | session="WebStreamer", 17 | api_id=Var.API_ID, 18 | api_hash=Var.API_HASH, 19 | flood_sleep_threshold=Var.SLEEP_THRESHOLD, 20 | receive_updates=not Var.NO_UPDATE 21 | ) 22 | 23 | async def initialize_clients(): 24 | multi_clients[0] = StreamBot 25 | work_loads[0] = 0 26 | all_tokens = dict( 27 | (c + 1, t) 28 | for c, (_, t) in enumerate( 29 | filter( 30 | lambda n: n[0].startswith("MULTI_TOKEN"), sorted(environ.items()) 31 | ) 32 | ) 33 | ) 34 | if not all_tokens: 35 | logging.info("No additional clients found, using default client") 36 | return 37 | 38 | async def start_client(client_id, token): 39 | try: 40 | logging.info("Starting - Client %s", client_id) 41 | if client_id == len(all_tokens): 42 | await asyncio.sleep(2) 43 | logging.info("This will take some time, please wait...") 44 | client = TelegramClient( 45 | session=MemorySession(), 46 | api_id=Var.API_ID, 47 | api_hash=Var.API_HASH, 48 | flood_sleep_threshold=Var.SLEEP_THRESHOLD, 49 | receive_updates=False 50 | ) 51 | await client.start(bot_token=token) 52 | await startup(client) 53 | try: 54 | await client.get_input_entity(Var.BIN_CHANNEL) 55 | except ValueError: 56 | logging.error("Client - %s is not in the Bin Channel.", client_id) 57 | work_loads[client_id] = 0 58 | return client_id, client 59 | except Exception: 60 | logging.error("Failed starting Client - %s Error:", client_id, exc_info=True) 61 | 62 | clients = await asyncio.gather(*[start_client(i, token) for i, token in all_tokens.items()]) 63 | multi_clients.update(dict(filter(None,clients))) 64 | if len(multi_clients) != 1: 65 | Var.MULTI_CLIENT = True 66 | logging.info("Multi-client mode enabled") 67 | else: 68 | logging.info("No additional clients were initialized, using default client") 69 | -------------------------------------------------------------------------------- /WebStreamer/plugins/start.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | # pylint: disable=relative-beyond-top-level 3 | 4 | from telethon.extensions import html 5 | from telethon.events import NewMessage 6 | from WebStreamer import __version__ 7 | from WebStreamer.clients import StreamBot 8 | from WebStreamer.vars import Var 9 | 10 | @StreamBot.on(NewMessage(incoming=True,pattern=r"^\/start*", func=lambda e: e.is_private)) 11 | async def start(event: NewMessage.Event): 12 | user = await event.get_sender() 13 | if (Var.ALLOWED_USERS and user.id not in Var.ALLOWED_USERS) or ( 14 | Var.BLOCKED_USERS and user.id in Var.BLOCKED_USERS): 15 | return await event.message.reply( 16 | message="You are not in the allowed list of users who can use me.", 17 | link_preview=False, 18 | parse_mode=html 19 | ) 20 | await event.message.reply( 21 | message=f'Hi {user.first_name}, Send me a file to get an instant stream link.', 22 | link_preview=False, 23 | parse_mode=html 24 | ) 25 | 26 | @StreamBot.on(NewMessage(incoming=True,pattern=r"^\/about*", func=lambda e: e.is_private)) 27 | async def about(event: NewMessage.Event): 28 | await event.message.reply( 29 | message=f""" 30 | Maintained By: DeekshithSH 31 | Source Code: TG-FileStreamBot 32 | Based On: [tg filestream] [TG-FileStreamBot] 33 | Version: {__version__} 34 | Last Updated: 08 April 2025 35 | """, 36 | link_preview=False, 37 | parse_mode=html 38 | ) -------------------------------------------------------------------------------- /WebStreamer/plugins/stream.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | # pylint: disable=relative-beyond-top-level 3 | 4 | import logging 5 | from telethon import Button, errors 6 | from telethon.events import NewMessage 7 | from telethon.extensions import html 8 | from WebStreamer.clients import StreamBot 9 | from WebStreamer.utils.file_properties import get_file_info, pack_file, get_short_hash 10 | from WebStreamer.vars import Var 11 | 12 | MEDIA={"video", "audio"} # we can expand it to include more media types 13 | 14 | @StreamBot.on(NewMessage(func=lambda e: True if e.message.file and e.is_private else False)) 15 | async def media_receive_handler(event: NewMessage.Event): 16 | user = await event.get_sender() 17 | if (Var.ALLOWED_USERS and user.id not in Var.ALLOWED_USERS) or ( 18 | Var.BLOCKED_USERS and user.id in Var.BLOCKED_USERS): 19 | return await event.message.reply( 20 | message="You are not in the allowed list of users who can use me.", 21 | link_preview=False, 22 | parse_mode=html 23 | ) 24 | try: 25 | log_msg=await event.message.forward_to(Var.BIN_CHANNEL) 26 | file_info=get_file_info(log_msg) 27 | full_hash = pack_file( 28 | file_info.file_name, 29 | file_info.file_size, 30 | file_info.mime_type, 31 | file_info.id 32 | ) 33 | file_hash=get_short_hash(full_hash) 34 | stream_link = f"{Var.URL}stream/{log_msg.id}?hash={file_hash}" 35 | is_media = bool(set(file_info.mime_type.split("/")) & MEDIA) 36 | buttons=[[Button.url("Open", url=stream_link)]] 37 | message=f"{stream_link}" 38 | if is_media: 39 | buttons.append([Button.url("Stream", url=stream_link+"&s=1")]) 40 | message+=f"(Stream)" 41 | await event.message.reply( 42 | message=message, 43 | link_preview=False, 44 | buttons=buttons, 45 | parse_mode=html 46 | ) 47 | except errors.FloodWaitError as e: 48 | logging.error(e) 49 | -------------------------------------------------------------------------------- /WebStreamer/stream_routes.py: -------------------------------------------------------------------------------- 1 | # Taken from megadlbot_oss 2 | # Thanks to Eyaadh 3 | 4 | import time 5 | import logging 6 | import mimetypes 7 | from aiohttp import web 8 | from aiohttp.http_exceptions import BadStatusLine 9 | from WebStreamer.clients import multi_clients, work_loads 10 | from WebStreamer.utils.file_properties import get_short_hash, pack_file 11 | from WebStreamer.utils.util import allow_request, get_requester_ip, get_readable_time 12 | from WebStreamer.vars import Var 13 | from WebStreamer import StartTime, __version__ 14 | from WebStreamer.utils.paralleltransfer import ParallelTransferrer 15 | 16 | 17 | routes = web.RouteTableDef() 18 | class_cache = {} 19 | 20 | @routes.get("/status", allow_head=True) 21 | async def root_route_handler(_: web.Request): 22 | return web.json_response( 23 | { 24 | "server_status": "running", 25 | "uptime": get_readable_time(time.time() - StartTime), 26 | "telegram_bot": "@" + Var.USERNAME, 27 | "connected_bots": len(multi_clients), 28 | "loads": dict( 29 | (f"bot{c + 1}", l) 30 | for c, l in ( 31 | sorted(work_loads.items()) 32 | ) 33 | ), 34 | "version": __version__, 35 | } 36 | ) 37 | 38 | 39 | @routes.get(r"/stream/{messageID:\d+}", allow_head=True) 40 | async def stream_handler(request: web.Request): 41 | try: 42 | message_id = int(request.match_info["messageID"]) 43 | secure_hash = request.rel_url.query.get("hash") 44 | return await media_streamer(request, message_id, secure_hash) 45 | except (AttributeError, BadStatusLine, ConnectionResetError): 46 | pass 47 | except Exception as e: 48 | logging.critical(str(e), exc_info=True) 49 | raise web.HTTPInternalServerError(text=str(e)) 50 | 51 | 52 | async def media_streamer(request: web.Request, message_id: int, secure_hash: str): 53 | head: bool = request.method == "HEAD" 54 | ip = get_requester_ip(request) 55 | range_header = request.headers.get("Range", 0) 56 | 57 | index = min(work_loads, key=work_loads.get) 58 | faster_client = multi_clients[index] 59 | 60 | if Var.MULTI_CLIENT: 61 | logging.debug("Client %s is now serving %s", index, ip) 62 | 63 | if faster_client in class_cache: 64 | transfer = class_cache[faster_client] 65 | logging.debug("Using cached ByteStreamer object for client %s", index) 66 | else: 67 | logging.debug("Creating new ByteStreamer object for client %s", index) 68 | transfer = ParallelTransferrer(faster_client) 69 | transfer.post_init() 70 | class_cache[faster_client] = transfer 71 | logging.debug("Created new ByteStreamer object for client %s", index) 72 | logging.debug("before calling get_file_properties") 73 | file_id = await transfer.get_file_properties(message_id) 74 | if not file_id: 75 | return web.Response(status=404, text="File not found") 76 | 77 | full_hash = pack_file( 78 | file_id.file_name, 79 | file_id.file_size, 80 | file_id.mime_type, 81 | file_id.id 82 | ) 83 | if get_short_hash(full_hash) != secure_hash: 84 | logging.debug("Invalid hash for message with ID %s", message_id) 85 | return web.HTTPForbidden(text="Invalid hash") 86 | 87 | file_size = file_id.file_size 88 | 89 | if range_header: 90 | from_bytes, until_bytes = range_header.replace("bytes=", "").split("-") 91 | from_bytes = int(from_bytes) 92 | until_bytes = int(until_bytes) if until_bytes else file_size - 1 93 | else: 94 | from_bytes = request.http_range.start or 0 95 | until_bytes = (request.http_range.stop or file_size) - 1 96 | 97 | if (until_bytes > file_size) or (from_bytes < 0) or (until_bytes < from_bytes): 98 | return web.Response( 99 | status=416, 100 | body="416: Range not satisfiable", 101 | headers={"Content-Range": f"bytes */{file_size}"}, 102 | ) 103 | until_bytes = min(until_bytes, file_size - 1) 104 | req_length = until_bytes - from_bytes + 1 105 | if not head: 106 | if not allow_request(ip): 107 | return web.Response(status=429) 108 | body = transfer.download( 109 | file_id, file_size, from_bytes, until_bytes, index, ip 110 | ) 111 | else: 112 | body = None 113 | 114 | mime_type = file_id.mime_type 115 | file_name = file_id.file_name 116 | disposition = "attachment" 117 | 118 | if not mime_type: 119 | mime_type = mimetypes.guess_type( 120 | file_name)[0] or "application/octet-stream" 121 | 122 | if request.rel_url.query.get("s"): 123 | disposition = "inline" 124 | 125 | return web.Response( 126 | status=206 if range_header else 200, 127 | body=body, 128 | headers={ 129 | "Content-Type": f"{mime_type}", 130 | "Content-Range": f"bytes {from_bytes}-{until_bytes}/{file_size}", 131 | "Content-Length": str(req_length), 132 | "Content-Disposition": f'{disposition}; filename="{file_name}"', 133 | "Accept-Ranges": "bytes", 134 | }, 135 | ) 136 | -------------------------------------------------------------------------------- /WebStreamer/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | -------------------------------------------------------------------------------- /WebStreamer/utils/file_properties.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | 3 | from dataclasses import dataclass 4 | import hashlib 5 | from typing import Optional, Union 6 | from telethon import TelegramClient 7 | from telethon.utils import get_input_location 8 | from telethon.tl import types 9 | from telethon.tl.patched import Message 10 | from WebStreamer.vars import Var 11 | 12 | @dataclass 13 | class FileInfo: 14 | __slots__ = ("file_size", "mime_type", "file_name", "id", "dc_id", "location") 15 | 16 | file_size: int 17 | mime_type: str 18 | file_name: str 19 | id: int 20 | dc_id: int 21 | location: Union[types.InputPhotoFileLocation, types.InputDocumentFileLocation] 22 | 23 | class HashableFileStruct: 24 | def __init__(self, file_name: str, file_size: int, mime_type: str, file_id: int): 25 | self.file_name = file_name 26 | self.file_size = file_size 27 | self.mime_type = mime_type 28 | self.file_id = file_id 29 | 30 | def pack(self) -> str: 31 | hasher = hashlib.md5() 32 | fields = [self.file_name, str(self.file_size), self.mime_type, str(self.file_id)] 33 | 34 | for field in fields: 35 | hasher.update(field.encode()) 36 | 37 | return hasher.hexdigest() 38 | 39 | async def get_file_ids(client: TelegramClient, chat_id: int, message_id: int) -> Optional[FileInfo]: 40 | message: Message = await client.get_messages(chat_id, ids=message_id) 41 | if not message: 42 | return None 43 | return get_file_info(message) 44 | 45 | def get_file_info(message: Message) -> FileInfo: 46 | media: Union[types.MessageMediaDocument, types.MessageMediaPhoto] = message.media 47 | file = getattr(media, "document", None) or getattr(media, "photo", None) 48 | return FileInfo( 49 | message.file.size, 50 | message.file.mime_type, 51 | getattr(message.file, "name", None) or "", 52 | file.id, 53 | *get_input_location(media) 54 | ) 55 | 56 | def pack_file(file_name: str, file_size: int, mime_type: str, file_id: int) -> str: 57 | return HashableFileStruct(file_name, file_size, mime_type, file_id).pack() 58 | 59 | def get_short_hash(file_hash: str) -> str: 60 | return file_hash[:Var.HASH_LENGTH] 61 | -------------------------------------------------------------------------------- /WebStreamer/utils/keepalive.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | 3 | import asyncio 4 | import logging 5 | import traceback 6 | import aiohttp 7 | from WebStreamer.vars 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: %s",resp.status) 20 | except TimeoutError: 21 | logging.warning("Couldn't connect to the site URL..!") 22 | except Exception: 23 | traceback.print_exc() 24 | -------------------------------------------------------------------------------- /WebStreamer/utils/paralleltransfer.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | # 3 | # Copyright (C) 2019 Tulir Asokan 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | # Modifications made by Deekshith SH, 2024-2025 19 | # Copyright (C) 2024-2025 Deekshith SH 20 | 21 | # pylint: disable=protected-access 22 | from collections import OrderedDict 23 | import copy 24 | from typing import AsyncGenerator, Optional 25 | from contextlib import asynccontextmanager 26 | from dataclasses import dataclass 27 | import logging 28 | import asyncio 29 | import math 30 | 31 | from telethon import TelegramClient 32 | from telethon.crypto import AuthKey 33 | from telethon.network import MTProtoSender 34 | from telethon.tl.functions import InvokeWithLayerRequest 35 | from telethon.tl.functions.auth import ExportAuthorizationRequest, ImportAuthorizationRequest 36 | from telethon.tl.functions.upload import GetFileRequest 37 | from telethon.tl.alltlobjects import LAYER 38 | from telethon.tl.types import DcOption 39 | from telethon.errors import DcIdInvalidError 40 | 41 | from WebStreamer.utils.util import decrement_counter, increment_counter 42 | from WebStreamer.utils.file_properties import FileInfo, get_file_ids 43 | from WebStreamer.vars import Var 44 | from WebStreamer.clients import work_loads 45 | 46 | root_log = logging.getLogger(__name__) 47 | 48 | if Var.CONNECTION_LIMIT > 25: 49 | root_log.warning("The connection limit should not be set above 25 to avoid" 50 | " infinite disconnect/reconnect loops") 51 | 52 | 53 | @dataclass 54 | class Connection: 55 | log: logging.Logger 56 | sender: MTProtoSender 57 | lock: asyncio.Lock 58 | users: int = 0 59 | 60 | 61 | class DCConnectionManager: 62 | log: logging.Logger 63 | client: TelegramClient 64 | 65 | dc_id: int 66 | dc: Optional[DcOption] 67 | auth_key: Optional[AuthKey] 68 | connections: list[Connection] 69 | 70 | _list_lock: asyncio.Lock 71 | 72 | def __init__(self, client: TelegramClient, dc_id: int) -> None: 73 | self.log = root_log.getChild(f"dc{dc_id}") 74 | self.client = client 75 | self.dc_id = dc_id 76 | self.auth_key = None 77 | self.connections = [] 78 | self._list_lock = asyncio.Lock() 79 | self.dc = None 80 | 81 | async def _new_connection(self) -> Connection: 82 | if not self.dc: 83 | self.dc = await self.client._get_dc(self.dc_id) 84 | sender = MTProtoSender(self.auth_key, loggers=self.client._log) 85 | index = len(self.connections) + 1 86 | conn = Connection(sender=sender, log=self.log.getChild(f"conn{index}"), lock=asyncio.Lock()) 87 | self.connections.append(conn) 88 | async with conn.lock: 89 | conn.log.info("Connecting...") 90 | connection_info = self.client._connection( 91 | self.dc.ip_address, self.dc.port, self.dc.id, 92 | loggers=self.client._log, 93 | proxy=self.client._proxy) 94 | await sender.connect(connection_info) 95 | if not self.auth_key: 96 | await self._export_auth_key(conn) 97 | return conn 98 | 99 | async def _export_auth_key(self, conn: Connection) -> None: 100 | self.log.info(f"Exporting auth to DC {self.dc.id}" 101 | f" (main client is in {self.client.session.dc_id})") 102 | try: 103 | auth = await self.client(ExportAuthorizationRequest(self.dc.id)) 104 | except DcIdInvalidError: 105 | self.log.debug("Got DcIdInvalidError") 106 | self.auth_key = self.client.session.auth_key 107 | conn.sender.auth_key = self.auth_key 108 | return 109 | init_request = copy.copy(self.client._init_request) 110 | init_request.query = ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes) 111 | req = InvokeWithLayerRequest(LAYER, init_request) 112 | await conn.sender.send(req) 113 | self.auth_key = conn.sender.auth_key 114 | 115 | async def _next_connection(self) -> Connection: 116 | best_conn: Optional[Connection] = None 117 | if self.connections: 118 | best_conn = min(self.connections, key=lambda conn: conn.users) 119 | if (not best_conn or best_conn.users > 0) and len(self.connections) < Var.CONNECTION_LIMIT: 120 | best_conn = await self._new_connection() 121 | return best_conn 122 | 123 | @asynccontextmanager 124 | async def get_connection(self) -> AsyncGenerator[Connection, None]: 125 | async with self._list_lock: 126 | conn: Connection = await asyncio.shield(self._next_connection()) 127 | # The connection is locked so reconnections don't stack 128 | async with conn.lock: 129 | conn.users += 1 130 | try: 131 | yield conn 132 | finally: 133 | conn.users -= 1 134 | 135 | 136 | class ParallelTransferrer: 137 | log: logging.Logger = logging.getLogger(__name__) 138 | client: TelegramClient 139 | lock: asyncio.Lock 140 | cached_files: OrderedDict[int, asyncio.Task] 141 | 142 | dc_managers: dict[int, DCConnectionManager] 143 | 144 | def __init__(self, client: TelegramClient) -> None: 145 | self.client = client 146 | self.dc_managers = { 147 | 1: DCConnectionManager(client, 1), 148 | 2: DCConnectionManager(client, 2), 149 | 3: DCConnectionManager(client, 3), 150 | 4: DCConnectionManager(client, 4), 151 | 5: DCConnectionManager(client, 5), 152 | } 153 | self.cached_files = OrderedDict() 154 | self.lock = asyncio.Lock() 155 | 156 | async def get_file_properties(self, message_id: int) -> Optional[FileInfo]: 157 | if message_id in self.cached_files: 158 | return await asyncio.shield(self.cached_files[message_id]) 159 | task=asyncio.create_task(get_file_ids(self.client, Var.BIN_CHANNEL, message_id)) 160 | if Var.CACHE_SIZE is not None and len(self.cached_files) > Var.CACHE_SIZE: 161 | self.cached_files.popitem(last=False) 162 | self.cached_files[message_id]=task 163 | file_id=await asyncio.shield(task) 164 | if not file_id: 165 | self.cached_files.pop(message_id) 166 | logging.debug("File not found for message with ID %s", message_id) 167 | return None 168 | logging.debug("Generated file ID for message with ID %s", message_id) 169 | return file_id 170 | 171 | def post_init(self) -> None: 172 | self.dc_managers[self.client.session.dc_id].auth_key = self.client.session.auth_key 173 | 174 | async def _int_download(self, request: GetFileRequest, dc_id: int,first_part_cut: int, 175 | last_part_cut: int, part_count: int, chunk_size: int, 176 | last_part: int, total_parts: int, index: int, ip: str) -> AsyncGenerator[bytes, None]: 177 | log = self.log 178 | try: 179 | async with self.lock: 180 | work_loads[index] += 1 181 | increment_counter(ip) 182 | current_part = 1 183 | dcm = self.dc_managers[dc_id] 184 | async with dcm.get_connection() as conn: 185 | log = conn.log 186 | while current_part <= part_count: 187 | result = await conn.sender.send(request) 188 | request.offset += chunk_size 189 | if not result.bytes: 190 | break 191 | elif part_count == 1: 192 | yield result.bytes[first_part_cut:last_part_cut] 193 | elif current_part == 1: 194 | yield result.bytes[first_part_cut:] 195 | elif current_part == part_count: 196 | yield result.bytes[:last_part_cut] 197 | else: 198 | yield result.bytes 199 | log.debug("Part %s/%s (total %s) downloaded",current_part,last_part,total_parts) 200 | current_part += 1 201 | log.debug("Parallel download finished") 202 | except (GeneratorExit, StopAsyncIteration, asyncio.CancelledError): 203 | log.debug("Parallel download interrupted") 204 | raise 205 | except Exception: 206 | log.debug("Parallel download errored", exc_info=True) 207 | finally: 208 | async with self.lock: 209 | work_loads[index] -= 1 210 | decrement_counter(ip) 211 | logging.debug("Finished yielding file with %s parts.",current_part) 212 | 213 | def download(self, file_id: FileInfo, file_size: int, from_bytes: int, until_bytes: int, index: int, ip: str 214 | ) -> AsyncGenerator[bytes, None]: 215 | dc_id = file_id.dc_id 216 | location=file_id.location 217 | 218 | chunk_size = Var.CHUNK_SIZE 219 | offset = from_bytes - (from_bytes % chunk_size) 220 | first_part_cut = from_bytes - offset 221 | first_part = math.floor(offset / chunk_size) 222 | last_part_cut = until_bytes % chunk_size + 1 223 | last_part = math.ceil(until_bytes / chunk_size) 224 | part_count = last_part - first_part 225 | total_parts = math.ceil(file_size / chunk_size) 226 | 227 | self.log.debug("Starting parallel download: chunks %s-%s" 228 | " of %s %s",first_part,last_part,part_count,location) 229 | request = GetFileRequest(location, offset=offset, limit=chunk_size) 230 | 231 | return self._int_download(request, dc_id, first_part_cut, last_part_cut, 232 | part_count, chunk_size, last_part, total_parts, index, ip) 233 | 234 | -------------------------------------------------------------------------------- /WebStreamer/utils/util.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | 3 | import logging 4 | import importlib.util 5 | from collections import defaultdict 6 | from pathlib import Path 7 | from aiohttp import web 8 | 9 | from telethon import TelegramClient 10 | from telethon.tl import functions 11 | from WebStreamer.vars import Var 12 | 13 | ongoing_requests: dict[str, int] = defaultdict(lambda: 0) 14 | 15 | def load_plugins(folder_path: str): 16 | folder = Path(folder_path) 17 | package_prefix = ".".join(folder.parts) 18 | for file in folder.glob("*.py"): 19 | module_name = f"{package_prefix}.{file.stem}" 20 | spec = importlib.util.spec_from_file_location(module_name, str(file)) 21 | module = importlib.util.module_from_spec(spec) 22 | module.__package__ = package_prefix 23 | spec.loader.exec_module(module) 24 | logging.info("Imported %s", module_name) 25 | 26 | 27 | def get_requester_ip(req: web.Request) -> str: 28 | if Var.TRUST_HEADERS: 29 | try: 30 | return req.headers["X-Forwarded-For"].split(", ")[0] 31 | except KeyError: 32 | pass 33 | peername = req.transport.get_extra_info('peername') 34 | if peername is not None: 35 | return peername[0] 36 | 37 | def allow_request(ip: str) -> None: 38 | return ongoing_requests[ip] < Var.REQUEST_LIMIT 39 | 40 | def increment_counter(ip: str) -> None: 41 | ongoing_requests[ip] += 1 42 | 43 | def decrement_counter(ip: str) -> None: 44 | ongoing_requests[ip] -= 1 45 | 46 | 47 | # Moved from utils/time_format.py 48 | def get_readable_time(seconds: int) -> str: 49 | count = 0 50 | readable_time = "" 51 | time_list = [] 52 | time_suffix_list = ["s", "m", "h", " days"] 53 | while count < 4: 54 | count += 1 55 | if count < 3: 56 | remainder, result = divmod(seconds, 60) 57 | else: 58 | remainder, result = divmod(seconds, 24) 59 | if seconds == 0 and remainder == 0: 60 | break 61 | time_list.append(int(result)) 62 | seconds = int(remainder) 63 | for x in range(len(time_list)): 64 | time_list[x] = str(time_list[x]) + time_suffix_list[x] 65 | if len(time_list) == 4: 66 | readable_time += time_list.pop() + ", " 67 | time_list.reverse() 68 | readable_time += ": ".join(time_list) 69 | return readable_time 70 | 71 | async def startup(client: TelegramClient): 72 | config = await client(functions.help.GetConfigRequest()) 73 | for option in config.dc_options: 74 | if option.ip_address == client.session.server_address: 75 | if client.session.dc_id != option.id: 76 | logging.warning("Fixed DC ID in session from %s to %s",client.session.dc_id,option.id) 77 | client.session.set_dc(option.id, option.ip_address, option.port) 78 | client.session.save() 79 | break 80 | # transfer.post_init() 81 | -------------------------------------------------------------------------------- /WebStreamer/vars.py: -------------------------------------------------------------------------------- 1 | # This file is a part of TG-FileStreamBot 2 | import sys 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 | USERNAME: str = None 12 | FIRST_NAME: str = None 13 | 14 | API_ID = int(environ.get("API_ID", 0)) 15 | API_HASH = str(environ.get("API_HASH", "")) 16 | BIN_CHANNEL = int( 17 | environ.get("BIN_CHANNEL", None) 18 | ) # you NEED to use a CHANNEL when you're using MULTI_CLIENT 19 | BOT_TOKEN = str(environ.get("BOT_TOKEN")) 20 | 21 | ALLOWED_USERS = [int(x.strip()) for x in environ.get("ALLOWED_USERS", "").split(",") if x.strip()] 22 | BIND_ADDRESS = str(environ.get("WEB_SERVER_BIND_ADDRESS", "0.0.0.0")) 23 | BLOCKED_USERS = [int(x.strip()) for x in environ.get("BLOCKED_USERS", "").split(",") if x.strip()] 24 | CACHE_SIZE: int = int(environ.get("CACHE_SIZE", 128)) 25 | CHUNK_SIZE: int = int(environ.get("CHUNK_SIZE", 1024 * 1024)) #bytes 26 | CONNECTION_LIMIT = int(environ.get("CONNECTION_LIMIT", 20)) 27 | DEBUG: bool = str(environ.get("DEBUG", "0").lower()) in ("1", "true", "t", "yes", "y") 28 | FQDN = str(environ.get("FQDN", BIND_ADDRESS)) 29 | HAS_SSL = str(environ.get("HAS_SSL", "0").lower()) in ("1", "true", "t", "yes", "y") 30 | HASH_LENGTH = int(environ.get("HASH_LENGTH", 6)) 31 | if not 5 < HASH_LENGTH < 64: 32 | sys.exit("Hash length should be greater than 5 and less than 64") 33 | KEEP_ALIVE = str(environ.get("KEEP_ALIVE", "0").lower()) in ("1", "true", "t", "yes", "y") 34 | NO_PORT = str(environ.get("NO_PORT", "0").lower()) in ("1", "true", "t", "yes", "y") 35 | NO_UPDATE = str(environ.get("NO_UPDATE", "0").lower()) in ("1", "true", "t", "yes", "y") 36 | PING_INTERVAL = int(environ.get("PING_INTERVAL", "600")) # 10 minutes 37 | PORT = int(environ.get("PORT", 8080)) 38 | REQUEST_LIMIT = int(environ.get("REQUEST_LIMIT", 5)) 39 | SLEEP_THRESHOLD = int(environ.get("SLEEP_THRESHOLD", "60")) # 1 minte 40 | TRUST_HEADERS: bool = str(environ.get("TRUST_HEADERS", "1").lower()) in ("1", "true", "t", "yes", "y") 41 | URL = f"http{"s" if HAS_SSL else ""}://{FQDN}{"" if NO_PORT else ":" + str(PORT)}/" 42 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "File Stream Bot", 3 | "description": "A Telethon Telegram bot to access Telegram Files via the web.", 4 | "keywords": [ 5 | "telegram", 6 | "web", 7 | "telethon", 8 | "aiohttp", 9 | "python", 10 | "plugin", 11 | "modular", 12 | "media" 13 | ], 14 | "repository": "https://github.com/DeekshithSH/TG-FileStreamBot", 15 | "success_url": "/status", 16 | "website": "https://github.com/DeekshithSH/TG-FileStreamBot", 17 | "env": { 18 | "API_ID": { 19 | "description": "Goto my.telegram.org to obtain this", 20 | "required": true 21 | }, 22 | "API_HASH": { 23 | "description": "Goto my.telegram.org to obtain this", 24 | "required": true 25 | }, 26 | "BOT_TOKEN": { 27 | "description": "Get the bot token from @BotFather", 28 | "required": true 29 | }, 30 | "BIN_CHANNEL": { 31 | "description": "Channel ID of your Bin Channel", 32 | "required": true 33 | }, 34 | "ALLOWED_USERS": { 35 | "description": "A list of user IDs separated by comma (,). If this is set, only the users in this list will be able to use the bot.", 36 | "required": false 37 | }, 38 | "CACHE_SIZE": { 39 | "description": "Maximum number of file info entries cached per client. Each client (including those using MULTI_TOKEN) gets its own separate cache of this size", 40 | "value": 128, 41 | "required": false 42 | }, 43 | "CHUNK_SIZE": { 44 | "description": "Size of the chunk to request from Telegram server when streaming a file", 45 | "required": false 46 | }, 47 | "CONNECTION_LIMIT": { 48 | "description": " (default 20) - The maximum number of connections to a single Telegram datacenter", 49 | "required": false 50 | }, 51 | "FQDN": { 52 | "description": "Heroku App URL or Custom Domain. Update it after deploying", 53 | "required": false 54 | }, 55 | "HAS_SSL": { 56 | "description": "(can be either True or False) If you want the generated links in https format", 57 | "value": "true", 58 | "required": false 59 | }, 60 | "KEEP_ALIVE": { 61 | "description": "(can be either True or False) If you want to make Heroku App ping itself every. Set to True if your using Eco Dyno", 62 | "required": false 63 | }, 64 | "NO_PORT": { 65 | "description": "(can be either True or False) If you don't want your port to be displayed. Leave it as it is", 66 | "value": "true", 67 | "required": false 68 | }, 69 | "NO_UPDATE": { 70 | "description": "if set to true bot won't respond to any messages", 71 | "required": false 72 | }, 73 | "PING_INTERVAL": { 74 | "description": "The time in seconds you want the servers to be pinged each time to avoid sleeping (Only for Heroku/Render). Defaults to 1200 or 10 minutes", 75 | "value": 1200, 76 | "required": false 77 | }, 78 | "REQUEST_LIMIT": { 79 | "description": " (default 5) - The maximum number of requests a single IP can have active at a time", 80 | "required": false 81 | }, 82 | "SLEEP_THRESHOLD": { 83 | "description": " 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.", 84 | "required": false 85 | }, 86 | "TRUST_HEADERS": { 87 | "description": "(defaults to true) - Whether or not to trust X-Forwarded-For headers when logging requests", 88 | "required": false 89 | }, 90 | "UPDATES_CHANNEL": { 91 | "description": "Your Telegram Channel Username without @", 92 | "required": false 93 | } 94 | }, 95 | "buildpacks": [{ 96 | "url": "heroku/python" 97 | }], 98 | "formation": { 99 | "web": { 100 | "quantity": 1, 101 | "size": "basic" 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | ## How to make your own 2 | 3 | ### 🚀 Deploy on Heroku 4 | 5 | [![Deploy To Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 6 | 7 | Then go to the [variables tab](/README.md#environment-variables) for info on setting up environment variables. 8 | 9 | ### 🖥 Host on VPS / Locally 10 | 11 | ```sh 12 | git clone https://github.com/DeekshithSH/TG-FileStreamBot 13 | cd TG-FileStreamBot 14 | python3 -m venv ./venv 15 | . ./venv/bin/activate 16 | pip3 install -r requirements.txt 17 | python3 -m WebStreamer 18 | ``` 19 | 20 | To stop the bot, press CTRL+C 21 | 22 | To keep it running 24/7 on VPS: 23 | 24 | ```sh 25 | sudo apt install tmux -y 26 | tmux 27 | python3 -m WebStreamer 28 | ``` 29 | 30 | You can now close the terminal and the bot will keep running. 31 | 32 | 33 | ## Environment Variables 34 | 35 | [Click here for Environment Variables](/README.md#environment-variables) 36 | 37 | If you're on Heroku, add these as Config Vars. 38 | If hosting locally, create a `.env` file in the root directory and add them there. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | telethon 2 | cryptg 3 | aiohttp 4 | python-dotenv --------------------------------------------------------------------------------