├── .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 | [](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
--------------------------------------------------------------------------------