From fa313ac81170a3e0a2914c3f0666510fffc37856 Mon Sep 17 00:00:00 2001 From: blank X Date: Sat, 16 Jan 2021 10:33:53 +0700 Subject: [PATCH] Initial commit --- .gitignore | 2 + LICENSE | 21 +++++++++++ README.md | 15 ++++++++ example-config.yaml | 8 ++++ requirements.txt | 4 ++ streamtg.py | 90 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 140 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 example-config.yaml create mode 100644 requirements.txt create mode 100644 streamtg.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0852913 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +streamtg.session +config.yaml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..211185a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 blank X + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..99e62ab --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# StreamTG + +Stream files from Telegram + +### Installation Instructions +1. Install `python3` +2. `pip3 install -r requirements.txt` +3. Copy example-config.yaml to config.yaml and edit it + +### Start +`python3 streamtg.py` +If you want to set a custom port, have a PORT environment variable + +### How to Use +http://localhost:8080/?chat_id=-1001289824958&message_id=5302&message_id=5304&token=DIi4aXHn440PTPXJE1yVyIoU2L4bLGiyjC1Fd7usKAMZYWVcp5p0P792G4YlbNnIcWCLypXbUFJkVzKqhh0AkJYSWqJbsAy8TjA diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..0fc573e --- /dev/null +++ b/example-config.yaml @@ -0,0 +1,8 @@ +telegram: + api_id: 0 + api_hash: https://my.telegram.org + bot_token: https://t.me/BotFather +# If authorized tokens does not exist, no authorization is required +authorized_tokens: + - DIi4aXHn440PTPXJE1yVyIoU2L4bLGiyjC1Fd7usKAMZYWVcp5p0P792G4YlbNnIcWCLypXbUFJkVzKqhh0AkJYSWqJbsAy8TjA + - RCywhzEkwqWmDjsqhBdNMwO1cQNi72SWsvAKZxdoeFylbioqKCZjEKyjgDXkYn6xeMQLj4dDq6QoonVAU1b1MDyOUX9CvT5W4MP diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fbef1f3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +telethon +aiohttp +cryptg +pyyaml diff --git a/streamtg.py b/streamtg.py new file mode 100644 index 0000000..8a4ce0b --- /dev/null +++ b/streamtg.py @@ -0,0 +1,90 @@ +import logging +logging.basicConfig(level=logging.INFO) + +import os +import yaml +from aiohttp import web +from telethon import TelegramClient +from telethon.utils import _get_file_info + +with open('config.yaml') as file: + config = yaml.safe_load(file) + +client = TelegramClient('streamtg', config['telegram']['api_id'], config['telegram']['api_hash']) +authorized_tokens = config.get('authorized_tokens') +port = os.environ.get('PORT', 8080) + +async def handler(request): + query = request.query + if authorized_tokens: + if 'token' not in query: + return web.Response(status=401, text='Unauthorized') + if query['token'] not in authorized_tokens: + return web.Response(status=403, text='Forbidden') + if 'chat_id' not in query: + return web.Response(status=400, text='Missing chat_id') + chat_id = query['chat_id'] + try: + chat_id = int(chat_id) + except ValueError: + pass + if 'message_id' not in query: + return web.Response(status=400, text='Missing message_id') + message_ids = query.getall('message_id') + if any(True for i in message_ids if not i.isnumeric() or i == '0'): + return web.Response(status=400, text='Invalid message_id') + message_ids = list(map(int, message_ids)) + messages = await client.get_messages(chat_id, ids=message_ids) + if any(True for i in messages if i is None): + return web.Response(status=400, text='At least one of the messages does not exist') + if any(True for i in messages if not i.media): + return web.Response(status=400, text='At least one of the messages do not contain media') + http_range = request.http_range + offset = http_range.start or 0 + end = http_range.stop + max_size = 0 + for i in messages: + max_size += _get_file_info(i).size + if end is None: + end = max_size + elif end > max_size: + return web.Response(status=416, text='Range end size is bigger than file sizes', headers={'Content-Range': f'bytes */{max_size}'}) + length = end - offset + + async def download(): + tmp_offset = offset + tmp_length = length + for i in messages: + if tmp_length < 1: + break + size = _get_file_info(i).size + if tmp_offset > size: + tmp_offset -= size + continue + async for chunk in client._iter_download(i, offset=tmp_offset, msg_data=(chat_id, i.id)): + yield chunk[:tmp_length] + tmp_length -= len(chunk) + if tmp_length < 1: + break + + return web.Response(status=206 if (end - offset != max_size) else 200, + body=download(), + headers={ + 'Content-Range': f'bytes {offset}-{end}/{max_size}', + 'Content-Length': str(end - offset), + 'Accept-Ranges': 'bytes' + } + ) + +app = web.Application() +app.add_routes([web.get('/', handler)]) + +async def main(): + await client.start(bot_token=config['telegram'].get('bot_token')) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, '127.0.0.1', port) + await site.start() + await client.run_until_disconnected() + +client.loop.run_until_complete(main())