diff --git a/README.md b/README.md index a7a79f9..3df31dc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@

๐ŸŒFile Stream Bot

An open-source Python Telegram bot to transmit Telegram files over HTTP. - -Demo Bot

## **๐Ÿ“‘ INDEX** @@ -34,7 +32,7 @@ For Linux: ``` sudo apt-get update && sudo apt-get install -y python3.11 git pip ``` -For macOS: +For MacOS: ``` brew install python@3.11 git ``` @@ -71,13 +69,13 @@ pip install -r requirements.txt **The variables provided below should either be completed within the [config.py](https://github.com/TheCaduceus/FileStreamBot/blob/main/bot/config.py) file or configured as environment variables.** * `API_ID`|`TELEGRAM_API_ID`: API ID of your Telegram account, can be obtained from [My Telegram](https://my.telegram.org). `int` * `API_HASH`|`TELEGRAM_API_HASH`: API hash of your Telegram account, can be obtained from [My Telegram](https://my.telegram.org). `str` -* `OWNER_ID`: ID of your Telegram account, can be obtained by sending **/info** to [@DrFileStreamBot](https://t.me/DrFileStreamBot). `int` +* `OWNER_ID`: ID of your Telegram account, can be obtained by sending **/info** to [@DumpJsonBot](https://t.me/DumpJsonBot). `int` * `ALLOWED_USER_IDS`: A list of Telegram account IDs (separated by spaces) that are permitted to use the bot. Leave this field empty to allow anyone to use it. `str` * `BOT_USERNAME`|`TELEGRAM_BOT_USERNAME`: Username of your Telegram bot, create one using [@BotFather](https://t.me/BotFather). `str` * `BOT_TOKEN`|`TELEGRAM_BOT_TOKEN`: Telegram API token of your bot, can be obtained from [@BotFather](https://t.me/BotFather). `str` -* `CHANNEL_ID`|`TELEGRAM_CHANNEL_ID`: ID of the channel where bot will forward all files received from users, can be obtained by forwarding any message from channel to [@ShowJsonBot](https://t.me/ShowJsonBot) and then looking from `forward_from_chat` key. `int` +* `CHANNEL_ID`|`TELEGRAM_CHANNEL_ID`: ID of the channel where bot will forward all files received from users, can be obtained by forwarding any message from channel to [@DumpJsonBot](https://t.me/DumpJsonBot) and then looking from `forward_from_chat` key. `int` * `BOT_WORKERS`: Number of updates bot should process from Telegram at once, by default to 10 updates. `int` -* `SECRET_CODE_LENGTH`: Number of characters that file code should contain, by default to 12 characters. `int` +* `SECRET_CODE_LENGTH`: Number of characters that file code should contain, by default to 24 characters. `int` * `BASE_URL`: Base URL that bot should use while generating file links, can be FQDN and by default to `127.0.0.1`. `str` * `BIND_ADDRESS`: Bind address for web server, by default to `0.0.0.0` to run on all possible addresses. `str` * `PORT`: Port for web server to run on, by default to `8080`. `int` diff --git a/bot/__init__.py b/bot/__init__.py index e17e9ea..05fc31e 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -1,15 +1,19 @@ -from telethon import TelegramClient +from hydrogram import Client from logging import getLogger from logging.config import dictConfig from .config import Telegram, LOGGER_CONFIG_JSON dictConfig(LOGGER_CONFIG_JSON) -version = 1.6 +version = 1.7 logger = getLogger('bot') -TelegramBot = TelegramClient( - session='bot', - api_id=Telegram.API_ID, - api_hash=Telegram.API_HASH +TelegramBot = Client( + name ='bot', + api_id = Telegram.API_ID, + api_hash = Telegram.API_HASH, + bot_token = Telegram.BOT_TOKEN, + plugins = {'root': 'bot/plugins'}, + workers = Telegram.BOT_WORKERS, + max_concurrent_transmissions = 1000 ) diff --git a/bot/__main__.py b/bot/__main__.py index 7c8d32e..1ecf629 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -1,22 +1,6 @@ -from importlib import import_module -from pathlib import Path -from bot import TelegramBot, logger -from bot.config import Telegram +from bot import TelegramBot from bot.server import server -def load_plugins(): - count = 0 - for path in Path('bot/plugins').rglob('*.py'): - import_module(f'bot.plugins.{path.stem}') - count += 1 - logger.info(f'Loaded {count} {"plugins" if count > 1 else "plugin"}.') - if __name__ == '__main__': - logger.info('initializing...') TelegramBot.loop.create_task(server.serve()) - TelegramBot.start(bot_token=Telegram.BOT_TOKEN) - logger.info('Telegram client is now started.') - logger.info('Loading bot plugins...') - load_plugins() - logger.info('Bot is now ready!') - TelegramBot.run_until_disconnected() + TelegramBot.run() diff --git a/bot/config.py b/bot/config.py index 49ffd6e..505d1f9 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,14 +1,15 @@ from os import environ as env class Telegram: - API_ID = int(env.get("TELEGRAM_API_ID", 1234)) + API_ID = int(env.get("TELEGRAM_API_ID", 12345)) API_HASH = env.get("TELEGRAM_API_HASH", "xyz") - OWNER_ID = int(env.get("OWNER_ID", 1234567890)) + OWNER_ID = int(env.get("OWNER_ID", 5530237028)) ALLOWED_USER_IDS = env.get("ALLOWED_USER_IDS", "").split() BOT_USERNAME = env.get("TELEGRAM_BOT_USERNAME", "BotFather") - BOT_TOKEN = env.get("TELEGRAM_BOT_TOKEN", "1234:abcd") - CHANNEL_ID = int(env.get("TELEGRAM_CHANNEL_ID", -1001234567890)) - SECRET_CODE_LENGTH = int(env.get("SECRET_CODE_LENGTH", 12)) + BOT_TOKEN = env.get("TELEGRAM_BOT_TOKEN", "1234567:xyz") + BOT_WORKERS = env.get("BOT_WORKERS", 10) + CHANNEL_ID = int(env.get("TELEGRAM_CHANNEL_ID", -100123456789)) + SECRET_CODE_LENGTH = int(env.get("SECRET_CODE_LENGTH", 24)) class Server: BASE_URL = env.get("BASE_URL", "http://127.0.0.1:8080") @@ -47,6 +48,10 @@ LOGGER_CONFIG_JSON = { 'bot': { 'level': 'INFO', 'handlers': ['file_handler', 'stream_handler'] + }, + 'hydrogram': { + 'level': 'INFO', + 'handlers': ['file_handler', 'stream_handler'] } } } diff --git a/bot/modules/decorators.py b/bot/modules/decorators.py index 5a34555..d065e3b 100644 --- a/bot/modules/decorators.py +++ b/bot/modules/decorators.py @@ -1,20 +1,16 @@ -from telethon.events import NewMessage, CallbackQuery -from typing import Callable +from hydrogram import Client +from hydrogram.types import Message, CallbackQuery +from typing import Union, Callable from functools import wraps from bot.config import Telegram -def verify_user(private: bool = False): - - def decorator(func: Callable): - @wraps(func) - async def wrapper(update: NewMessage.Event | CallbackQuery.Event): - if private and not update.is_private: - return +def verify_user(func: Callable): - chat_id = str(update.chat_id) + @wraps(func) + async def decorator(client: Client, update: Union[Message, CallbackQuery]): + chat_id = str(update.from_user.id if update.from_user else update.chat.id) - if not Telegram.ALLOWED_USER_IDS or chat_id in Telegram.ALLOWED_USER_IDS: - return await func(update) - - return wrapper + if not Telegram.ALLOWED_USER_IDS or chat_id in Telegram.ALLOWED_USER_IDS: + return await func(client, update) + return decorator diff --git a/bot/modules/static.py b/bot/modules/static.py index dfd9865..c93d720 100644 --- a/bot/modules/static.py +++ b/bot/modules/static.py @@ -1,35 +1,17 @@ WelcomeText = \ """ -Hi **%(first_name)s**, send me a file or add me as an admin to any channel to instantly generate file links. - -Add me to your channel to instantly generate links for any downloadable media. Once received, I will automatically attach appropriate buttons to the post containing the URL. If you want me to ignore a given post, you can insert `#pass` in the post. - -- /start to get this message. -- /info to get user info. -- /log to get bot logs. (admin only!) +Hi **%(first_name)s**, send me a file to instantly generate file links. """ -UserInfoText = \ +PrivacyText = \ """ -**First Name:** -`{sender.first_name}` - -**Last Name:** -`{sender.last_name}` - -**User ID:** -`{sender.id}` - -**Username:** -`@{sender.username}` +This bot securely stores your files to deliver its service. """ FileLinksText = \ """ **Download Link:** `%(dl_link)s` -**Telegram File:** -`%(tg_link)s` """ MediaLinksText = \ @@ -38,8 +20,6 @@ MediaLinksText = \ `%(dl_link)s` **Stream Link:** `%(stream_link)s` -**Telegram File:** -`%(tg_link)s` """ InvalidQueryText = \ @@ -61,8 +41,3 @@ InvalidPayloadText = \ """ Invalid payload. """ - -MediaTypeNotSupportedText = \ -""" -Sorry, this media type is not supported. -""" diff --git a/bot/modules/telegram.py b/bot/modules/telegram.py index 2867d54..f50007c 100644 --- a/bot/modules/telegram.py +++ b/bot/modules/telegram.py @@ -1,5 +1,4 @@ -from telethon.events import NewMessage -from telethon.tl.custom import Message +from hydrogram.types import Message from datetime import datetime from mimetypes import guess_type from bot import TelegramBot @@ -10,55 +9,47 @@ async def get_message(message_id: int) -> Message | None: message = None try: - message = await TelegramBot.get_messages(Telegram.CHANNEL_ID, ids=message_id) + message = await TelegramBot.get_messages(Telegram.CHANNEL_ID, message_ids=message_id) + if message.empty: message = None except Exception: pass return message -async def send_message(message:Message, send_to:int = Telegram.CHANNEL_ID) -> Message: - return await TelegramBot.send_message(entity=send_to, message=message) +async def send_message(msg: Message, send_to: int = Telegram.CHANNEL_ID) -> Message: + return await TelegramBot.send_message(entity=send_to, message=msg) -def filter_files(update: NewMessage.Event | Message): - return bool( - ( - update.document - or update.photo - or update.video - or update.video_note - or update.audio - or update.gif - ) - and not update.sticker +def get_file_properties(msg: Message): + attributes = ( + 'document', + 'video', + 'audio', + 'voice', + 'photo', + 'video_note' ) + for attribute in attributes: + media = getattr(msg, attribute, None) + if media: + file_type = attribute + break -def get_file_properties(message: Message): - file_name = message.file.name - file_size = message.file.size or 0 - mime_type = message.file.mime_type + if not media: abort(400, 'Unknown file type.') + + file_name = getattr(media, 'file_name', None) + file_size = getattr(media, 'file_size', 0) if not file_name: - attributes = { + file_format = { 'video': 'mp4', 'audio': 'mp3', 'voice': 'ogg', 'photo': 'jpg', 'video_note': 'mp4' - } - - for attribute in attributes: - media = getattr(message, attribute, None) - if media: - file_type, file_format = attribute, attributes[attribute] - break - - if not media: - abort(400, 'Invalid media type.') - + }.get(file_type) date = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") file_name = f'{file_type}-{date}.{file_format}' - if not mime_type: - mime_type = guess_type(file_name)[0] or 'application/octet-stream' - + mime_type = guess_type(file_name)[0] or 'application/octet-stream' + return file_name, file_size, mime_type diff --git a/bot/plugins/callback.py b/bot/plugins/callback.py index a8e630f..dee638a 100644 --- a/bot/plugins/callback.py +++ b/bot/plugins/callback.py @@ -1,24 +1,31 @@ -from telethon.events import CallbackQuery +from hydrogram.types import CallbackQuery from bot import TelegramBot from bot.modules.decorators import verify_user from bot.modules.static import * from bot.modules.telegram import get_message -@TelegramBot.on(CallbackQuery(pattern=r'^rm_')) -@verify_user(private=True) -async def delete_file(event: CallbackQuery.Event): - query_data = event.query.data.decode().split('_') +@TelegramBot.on_callback_query() +@verify_user +async def manage_callback(bot, q: CallbackQuery): + query = q.data - if len(query_data) != 3: - return await event.answer(InvalidQueryText, alert=True) + if query.startswith('rm_'): + sq = query.split('_') - message = await get_message(int(query_data[1])) + if len(sq) != 3: + return await q.answer(InvalidQueryText, show_alert=True) - if not message: - return await event.answer(MessageNotExist, alert=True) - if query_data[2] != message.raw_text: - return await event.answer(InvalidQueryText, alert=True) + message = await get_message(int(sq[1])) - await message.delete() + if not message: + return await q.answer(MessageNotExist, show_alert=True) + + sc = message.caption.split('/') - return await event.answer(LinkRevokedText, alert=True) + if q.from_user.id != int(sc[1]) or sq[2] != sc[0]: + return await q.answer(InvalidQueryText, show_alert=True) + + await message.delete() + await q.answer(LinkRevokedText, show_alert=True) + else: + await q.answer(InvalidQueryText, show_alert=True) diff --git a/bot/plugins/commands.py b/bot/plugins/commands.py index d00e648..4c76db1 100644 --- a/bot/plugins/commands.py +++ b/bot/plugins/commands.py @@ -1,28 +1,18 @@ -from telethon import Button -from telethon.events import NewMessage -from telethon.tl.custom.message import Message +from hydrogram import filters +from hydrogram.types import Message from bot import TelegramBot -from bot.config import Telegram from bot.modules.static import * from bot.modules.decorators import verify_user -@TelegramBot.on(NewMessage(incoming=True, pattern=r'^/start$')) -@verify_user(private=True) -async def welcome(event: NewMessage.Event | Message): - await event.reply( - message=WelcomeText % {'first_name': event.sender.first_name}, - buttons=[ - [ - Button.url('Add to Channel', f'https://t.me/{Telegram.BOT_USERNAME}?startchannel&admin=post_messages+edit_messages+delete_messages') - ] - ] +@TelegramBot.on_message(filters.command(['start', 'help']) & filters.private) +@verify_user +async def start_command(_, msg: Message): + await msg.reply( + text=WelcomeText % {'first_name': msg.from_user.first_name}, + quote=True ) -@TelegramBot.on(NewMessage(incoming=True, pattern=r'^/info$')) -@verify_user(private=True) -async def user_info(event: Message): - await event.reply(UserInfoText.format(sender=event.sender)) - -@TelegramBot.on(NewMessage(chats=Telegram.OWNER_ID, incoming=True, pattern=r'^/log$')) -async def send_log(event: NewMessage.Event | Message): - await event.reply(file='event-log.txt') +@TelegramBot.on_message(filters.command('privacy') & filters.private) +@verify_user +async def privacy_command(_, msg: Message): + await msg.reply(text=PrivacyText, quote=True) diff --git a/bot/plugins/deeplinks.py b/bot/plugins/deeplinks.py index 9e3c56e..eb6a004 100644 --- a/bot/plugins/deeplinks.py +++ b/bot/plugins/deeplinks.py @@ -1,24 +1,25 @@ -from telethon.events import NewMessage -from telethon.tl.custom import Message -from bot import TelegramBot -from bot.modules.decorators import verify_user -from bot.modules.telegram import get_message, send_message -from bot.modules.static import * +# Currently not in use. -@TelegramBot.on(NewMessage(incoming=True, pattern=r'^/start file_')) -@verify_user(private=True) -async def send_file(event: NewMessage.Event | Message): - payload = event.raw_text.split()[-1].split('_') +# from hydrogram.types import Message +# from bot import TelegramBot +# from bot.modules.decorators import verify_user +# from bot.modules.telegram import get_message, send_message +# from bot.modules.static import * - if len(payload) != 3: - return await event.reply(InvalidPayloadText) - - message = await get_message(int(payload[1])) +# async def deeplinks(msg: Message, payload: str): +# if payload.startswith('file_'): +# sp = payload.split('_') - if not message: - return await event.reply(MessageNotExist) - if payload[2] != message.raw_text: - return await event.reply(InvalidPayloadText) - - message.raw_text = '' - await send_message(message, send_to=event.chat_id) +# if len(sp) != 3: +# return await msg.reply(InvalidPayloadText, quote=True) + +# message = await get_message(int(sp[1])) + +# if not message: +# return await msg.reply(MessageNotExist) +# if sp[2] != message.caption: +# return await msg.reply(InvalidPayloadText, quote=True) + +# await message.copy(chat_id=msg.from_user.id, caption="") +# else: +# await msg.reply(InvalidPayloadText, quote=True) diff --git a/bot/plugins/files.py b/bot/plugins/files.py index d5a4e95..abd8161 100644 --- a/bot/plugins/files.py +++ b/bot/plugins/files.py @@ -1,95 +1,60 @@ -from telethon import Button -from telethon.events import NewMessage -from telethon.errors import MessageAuthorRequiredError, MessageNotModifiedError, MessageIdInvalidError -from telethon.tl.custom import Message +from hydrogram import filters +from hydrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton from secrets import token_hex from bot import TelegramBot from bot.config import Telegram, Server from bot.modules.decorators import verify_user -from bot.modules.telegram import send_message, filter_files from bot.modules.static import * -@TelegramBot.on(NewMessage(incoming=True, func=filter_files)) -@verify_user(private=True) -async def user_file_handler(event: NewMessage.Event | Message): +@TelegramBot.on_message( + filters.private + & ( + filters.document + | filters.video + | filters.video_note + | filters.audio + | filters.voice + | filters.photo + ) +) +@verify_user +async def handle_user_file(_, msg: Message): + sender_id = msg.from_user.id secret_code = token_hex(Telegram.SECRET_CODE_LENGTH) - event.message.text = f'`{secret_code}`' - message = await send_message(event.message) - message_id = message.id + file = await msg.copy( + chat_id=Telegram.CHANNEL_ID, + caption=f'||{secret_code}/{sender_id}||' + ) + file_id = file.id + dl_link = f'{Server.BASE_URL}/dl/{file_id}?code={secret_code}' - dl_link = f'{Server.BASE_URL}/dl/{message_id}?code={secret_code}' - tg_link = f'{Server.BASE_URL}/file/{message_id}?code={secret_code}' - deep_link = f'https://t.me/{Telegram.BOT_USERNAME}?start=file_{message_id}_{secret_code}' - - if (event.document and 'video' in event.document.mime_type) or event.video: - stream_link = f'{Server.BASE_URL}/stream/{message_id}?code={secret_code}' - await event.reply( - message= MediaLinksText % {'dl_link': dl_link, 'tg_link': tg_link, 'tg_link': tg_link, 'stream_link': stream_link}, - buttons=[ + if (msg.document and 'video' in msg.document.mime_type) or msg.video: + stream_link = f'{Server.BASE_URL}/stream/{file_id}?code={secret_code}' + await msg.reply( + text=MediaLinksText % {'dl_link': dl_link, 'stream_link': stream_link}, + quote=True, + reply_markup=InlineKeyboardMarkup( [ - Button.url('Download', dl_link), - Button.url('Stream', stream_link) - ], - [ - Button.url('Get File', deep_link), - Button.inline('Revoke', f'rm_{message_id}_{secret_code}') - ] - ] - ) - else: - await event.reply( - message=FileLinksText % {'dl_link': dl_link, 'tg_link': tg_link}, - buttons=[ - [ - Button.url('Download', dl_link), - Button.url('Get File', deep_link) - ], - [ - Button.inline('Revoke', f'rm_{message_id}_{secret_code}') - ] - ] - ) - -@TelegramBot.on(NewMessage(incoming=True, func=filter_files, forwards=False)) -@verify_user() -async def channel_file_handler(event: NewMessage.Event | Message): - if event.raw_text and '#pass' in event.raw_text: - return - - secret_code = token_hex(Telegram.SECRET_CODE_LENGTH) - event.message.text = f"`{secret_code}`" - message = await send_message(event.message) - message_id = message.id - - dl_link = f"{Server.BASE_URL}/dl/{message_id}?code={secret_code}" - tg_link = f"{Server.BASE_URL}/file/{message_id}?code={secret_code}" - - if (event.document and "video" in event.document.mime_type) or event.video: - stream_link = f"{Server.BASE_URL}/stream/{message_id}?code={secret_code}" - - try: - await event.edit( - buttons=[ - [Button.url("Download", dl_link), Button.url("Stream", stream_link)], - [Button.url("Get File", tg_link)], + [ + InlineKeyboardButton('Download', url=dl_link), + InlineKeyboardButton('Stream', url=stream_link) + ], + [ + InlineKeyboardButton('Revoke', callback_data=f'rm_{file_id}_{secret_code}') + ] ] ) - except ( - MessageAuthorRequiredError, - MessageIdInvalidError, - MessageNotModifiedError, - ): - pass + ) else: - try: - await event.edit( - buttons=[ - [Button.url("Download", dl_link), Button.url("Get File", tg_link)] + await msg.reply( + text=FileLinksText % {'dl_link': dl_link}, + quote=True, + reply_markup=InlineKeyboardMarkup( + [ + [ + InlineKeyboardButton('Download', url=dl_link), + InlineKeyboardButton('Revoke', callback_data=f'rm_{file_id}_{secret_code}') + ] ] ) - except ( - MessageAuthorRequiredError, - MessageIdInvalidError, - MessageNotModifiedError, - ): - pass + ) diff --git a/bot/server/__init__.py b/bot/server/__init__.py index 13a8f36..ab4e2f7 100644 --- a/bot/server/__init__.py +++ b/bot/server/__init__.py @@ -2,12 +2,12 @@ from quart import Quart from uvicorn import Server as UvicornServer, Config from logging import getLogger from bot.config import Server, LOGGER_CONFIG_JSON - from . import main, error logger = getLogger('uvicorn') instance = Quart(__name__) instance.config['RESPONSE_TIMEOUT'] = None +instance.config['MAX_CONTENT_LENGTH'] = 999999999999999 @instance.before_serving async def before_serve(): diff --git a/bot/server/main.py b/bot/server/main.py index b736778..d64aeba 100644 --- a/bot/server/main.py +++ b/bot/server/main.py @@ -1,8 +1,9 @@ from quart import Blueprint, Response, request, render_template, redirect +from math import ceil +from re import match as re_match from .error import abort from bot import TelegramBot from bot.config import Telegram, Server -from math import ceil, floor from bot.modules.telegram import get_message, get_file_properties bp = Blueprint('main', __name__) @@ -13,73 +14,70 @@ async def home(): @bp.route('/dl/') async def transmit_file(file_id): - file = await get_message(message_id=int(file_id)) or abort(404) + file = await get_message(file_id) or abort(404) code = request.args.get('code') or abort(401) - range_header = request.headers.get('Range', 0) + range_header = request.headers.get('Range') - if code != file.raw_text: + if code != file.caption.split('/')[0]: abort(403) file_name, file_size, mime_type = get_file_properties(file) - + + start = 0 + end = file_size - 1 + chunk_size = 1 * 1024 * 1024 # 1 MB + if range_header: - from_bytes, until_bytes = range_header.replace("bytes=", "").split("-") - from_bytes = int(from_bytes) - until_bytes = int(until_bytes) if until_bytes else file_size - 1 - else: - from_bytes = 0 - until_bytes = file_size - 1 + range_match = re_match(r'bytes=(\d+)-(\d*)', range_header) + if range_match: + start = int(range_match.group(1)) + end = int(range_match.group(2)) if range_match.group(2) else file_size - 1 + if start > end or start >= file_size: + abort(416, 'Requested range not satisfiable') + else: + abort(400, 'Invalid Range header') - if (until_bytes > file_size) or (from_bytes < 0) or (until_bytes < from_bytes): - abort(416, 'Invalid range.') + offset_chunks = start // chunk_size + total_bytes_to_stream = end - start + 1 + chunks_to_stream = ceil(total_bytes_to_stream / chunk_size) - chunk_size = 1024 * 1024 - until_bytes = min(until_bytes, file_size - 1) - - offset = from_bytes - (from_bytes % chunk_size) - first_part_cut = from_bytes - offset - last_part_cut = until_bytes % chunk_size + 1 - - req_length = until_bytes - from_bytes + 1 - part_count = ceil(until_bytes / chunk_size) - floor(offset / chunk_size) - + content_length = total_bytes_to_stream headers = { - "Content-Type": f"{mime_type}", - "Content-Range": f"bytes {from_bytes}-{until_bytes}/{file_size}", - "Content-Length": str(req_length), - "Content-Disposition": f'attachment; filename="{file_name}"', - "Accept-Ranges": "bytes", - } + 'Content-Type': mime_type, + 'Content-Disposition': f'attachment; filename={file_name}', + 'Content-Range': f'bytes {start}-{end}/{file_size}', + 'Accept-Ranges': 'bytes', + 'Content-Length': str(content_length), + } + status_code = 206 if range_header else 200 - async def file_generator(): - current_part = 1 - async for chunk in TelegramBot.iter_download(file, offset=offset, chunk_size=chunk_size, stride=chunk_size, file_size=file_size): - if not chunk: - break - elif part_count == 1: - yield chunk[first_part_cut:last_part_cut] - elif current_part == 1: - yield chunk[first_part_cut:] - elif current_part == part_count: - yield chunk[:last_part_cut] - else: - yield chunk + async def file_stream(): + bytes_streamed = 0 + chunk_index = 0 + async for chunk in TelegramBot.stream_media( + file, + offset=offset_chunks, + limit=chunks_to_stream, + ): + if chunk_index == 0: # Trim the first chunk if necessary + trim_start = start % chunk_size + if trim_start > 0: + chunk = chunk[trim_start:] - current_part += 1 - - if current_part > part_count: + remaining_bytes = content_length - bytes_streamed + if remaining_bytes <= 0: break - return Response(file_generator(), headers=headers, status=206 if range_header else 200) + if len(chunk) > remaining_bytes: # Trim the last chunk if necessary + chunk = chunk[:remaining_bytes] + + yield chunk + bytes_streamed += len(chunk) + chunk_index += 1 + + return Response(file_stream(), headers=headers, status=status_code) @bp.route('/stream/') async def stream_file(file_id): code = request.args.get('code') or abort(401) - return await render_template('player.html', mediaLink=f'{Server.BASE_URL}/dl/{file_id}?code={code}') - -@bp.route('/file/') -async def file_deeplink(file_id): - code = request.args.get('code') or abort(401) - - return redirect(f'https://t.me/{Telegram.BOT_USERNAME}?start=file_{file_id}_{code}') diff --git a/requirements.txt b/requirements.txt index 960ce6d..1039ad5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -telethon -cryptg +hydrogram +tgcrypto quart uvicorn