From a995905558e2e52cb867f940a9acf80145fd752e Mon Sep 17 00:00:00 2001 From: masoudtestdeploy <95133886+masoudtestdeploy@users.noreply.github.com> Date: Wed, 29 Jun 2022 22:07:00 +0430 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 117 ++++++++++++ .replit | 2 + Procfile | 1 + README.md | 138 ++++++++++++++ WebStreamer/__init__.py | 1 + WebStreamer/__main__.py | 64 +++++++ WebStreamer/bot/__init__.py | 12 ++ WebStreamer/bot/plugins/admin.py | 90 +++++++++ WebStreamer/bot/plugins/start.py | 261 ++++++++++++++++++++++++++ WebStreamer/bot/plugins/stream.py | 134 +++++++++++++ WebStreamer/server/__init__.py | 9 + WebStreamer/server/stream_routes.py | 79 ++++++++ WebStreamer/utils/__init__.py | 1 + WebStreamer/utils/broadcast_helper.py | 21 +++ WebStreamer/utils/custom_dl.py | 231 +++++++++++++++++++++++ WebStreamer/utils/database.py | 36 ++++ WebStreamer/utils/human_readable.py | 13 ++ WebStreamer/utils/keepalive.py | 18 ++ WebStreamer/vars.py | 33 ++++ app.json | 98 ++++++++++ requirements.txt | 7 + 22 files changed, 1368 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .replit create mode 100644 Procfile create mode 100644 README.md create mode 100644 WebStreamer/__init__.py create mode 100644 WebStreamer/__main__.py create mode 100644 WebStreamer/bot/__init__.py create mode 100644 WebStreamer/bot/plugins/admin.py create mode 100644 WebStreamer/bot/plugins/start.py create mode 100644 WebStreamer/bot/plugins/stream.py create mode 100644 WebStreamer/server/__init__.py create mode 100644 WebStreamer/server/stream_routes.py create mode 100644 WebStreamer/utils/__init__.py create mode 100644 WebStreamer/utils/broadcast_helper.py create mode 100644 WebStreamer/utils/custom_dl.py create mode 100644 WebStreamer/utils/database.py create mode 100644 WebStreamer/utils/human_readable.py create mode 100644 WebStreamer/utils/keepalive.py create mode 100644 WebStreamer/vars.py create mode 100644 app.json create mode 100644 requirements.txt diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b9d0a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +#session files +*.session \ No newline at end of file diff --git a/.replit b/.replit new file mode 100644 index 0000000..75cf641 --- /dev/null +++ b/.replit @@ -0,0 +1,2 @@ +language = "bash" +run = "pip3 install -r requirements.txt; python3 -m WebStreamer" \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..9a79e65 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python -m WebStreamer \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6378b1 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +

FileStreamBot

+

+ + Cover Image + + +

+ A Telegram bot to all media and documents files to web link . +
+ +
+ Report a Bug + | + Request Feature +

+

+ + +## 🍁 About This Bot : + +

+ + FileStreamBot Logo + +

+

+ This bot will give you stream links for Telegram files without the need of waiting till the download completes +

+ + +## ♢ How to make your own : + +Either you could locally host or deploy on [Heroku](https://heroku.com) + +#### ♢ Click on This Drop-down and get more details + +
+
+ Deploy on Heroku : + + +1. Fork This Repo +2. Click on Deploy Easily + +

So Follow Above Steps 👆 and then also deply other wise not work

+ +Press the below button to Fast deploy on Heroku + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) + +then goto the variables tab for more info on setting up environmental variables.
+ + +
+ Host it on VPS Locally : + + +```py +git clone https://github.com/avipatilpro/FileStreamBot +cd FileStreamBot +virtualenv -p /usr/bin/python3 venv +. ./venv/bin/activate +pip install -r requirements.txt +python3 -m WebStreamer +``` + +and to stop the whole bot, + do CTRL+C + +Setting up things + +If you're on Heroku, just add these in the Environmental Variables +or if you're Locally hosting, create a file named `.env` in the root directory and add all the variables there. +An example of `.env` file: + +```py +API_ID=12345 +API_HASH=esx576f8738x883f3sfzx83 +BOT_TOKEN=55838383:yourtbottokenhere +BIN_CHANNEL=-100 +PORT=8080 +FQDN=your_server_ip +OWNER_ID=your_user_id +DATABASE_URL=mongodb_uri +``` +
+ +
+ Vars and Details : + +`API_ID` : Goto [my.telegram.org](https://my.telegram.org) to obtain this. + +`API_HASH` : Goto [my.telegram.org](https://my.telegram.org) to obtain this. + +`BOT_TOKEN` : Get the bot token from [@BotFather](https://telegram.dog/BotFather) + +`BIN_CHANNEL` : Create a new channel (private/public), add [@missrose_bot](https://telegram.dog/MissRose_bot) as admin to the channel and type /id. Now copy paste the ID into this field. + +`OWNER_ID` : Your Telegram User ID + +`DATABASE_URL` : MongoDB URI for saving User IDs when they first Start the Bot. We will use that for Broadcasting to them. I will try to add more features related with Database. If you need help to get the URI you can ask in [Me Telegram](https://t.me/Avishkarpatil). + + Option Vars + +`UPDATES_CHANNEL` : Put a Public Channel Username, so every user have to Join that channel to use the bot. Must add bot to channel as Admin to work properly. + +`BANNED_CHANNELS` : Put IDs of Banned Channels where bot will not work. You can add multiple IDs & separate with Space. + +`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. + +`WORKERS` : Number of maximum concurrent workers for handling incoming updates. Defaults to `3` + +`PORT` : The port that you want your webapp to be listened to. Defaults to `8080` + +`WEB_SERVER_BIND_ADDRESS` : Your server bind adress. Defauls to `0.0.0.0` + +`NO_PORT` : 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. + +`FQDN` : A Fully Qualified Domain Name if present. Defaults to `WEB_SERVER_BIND_ADDRESS`
+ +
+ How to Use : + +:warning: **Before using the bot, don't forget to add the bot to the `BIN_CHANNEL` as an Admin** + +`/start` : To check if the bot is alive or not. + +To get an instant stream link, just forward any media to the bot and boom, its fast af. + +### Channel Support +Bot also Supported with Channels. Just add bot Channel as Admin. If any new file comes in Channel it will edit it with **Get Download Link** Button.
+ + +--- +

© 2022 Aνιѕнкαя Pαтιℓ

+ + + diff --git a/WebStreamer/__init__.py b/WebStreamer/__init__.py new file mode 100644 index 0000000..ff0636f --- /dev/null +++ b/WebStreamer/__init__.py @@ -0,0 +1 @@ +# This file is a part of avipatilpro/FileStreamBot diff --git a/WebStreamer/__main__.py b/WebStreamer/__main__.py new file mode 100644 index 0000000..b6192a8 --- /dev/null +++ b/WebStreamer/__main__.py @@ -0,0 +1,64 @@ + +import os +import sys +import glob +import asyncio +import logging +import importlib +from pathlib import Path +from pyrogram import idle +from .bot import StreamBot +from .vars import Var +from aiohttp import web +from .server import web_server +from .utils.keepalive import ping_server + +ppath = "WebStreamer/bot/plugins/*.py" +files = glob.glob(ppath) + +loop = asyncio.get_event_loop() + + +async def start_services(): + print('\n') + print('------------------- Initalizing Telegram Bot -------------------') + await StreamBot.start() + print('\n') + print('---------------------- DONE ----------------------') + print('\n') + print('------------------- Importing -------------------') + for name in files: + with open(name) as a: + patt = Path(a.name) + plugin_name = patt.stem.replace(".py", "") + plugins_dir = Path(f"WebStreamer/bot/plugins/{plugin_name}.py") + import_path = ".plugins.{}".format(plugin_name) + spec = importlib.util.spec_from_file_location(import_path, plugins_dir) + load = importlib.util.module_from_spec(spec) + spec.loader.exec_module(load) + sys.modules["WebStreamer.bot.plugins." + plugin_name] = load + print("Imported => " + plugin_name) + print('\n') + print('------------------- Initalizing Web Server -------------------') + app = web.AppRunner(await web_server()) + await app.setup() + bind_address = "0.0.0.0" if Var.ON_HEROKU else Var.FQDN + await web.TCPSite(app, bind_address, Var.PORT).start() + print('\n') + print('----------------------- Service Started -----------------------') + print(' bot =>> {}'.format((await StreamBot.get_me()).first_name)) + print(' server ip =>> {}:{}'.format(bind_address, Var.PORT)) + if Var.ON_HEROKU: + print(' app runnng on =>> {}'.format(Var.FQDN)) + if Var.ON_HEROKU: + print('------------------ Starting Keep Alive Service ------------------') + print('\n') + await asyncio.create_task(ping_server()) + print('---------------------------------------------------------------') + await idle() + +if __name__ == '__main__': + try: + loop.run_until_complete(start_services()) + except KeyboardInterrupt: + print('----------------------- Service Stopped -----------------------') diff --git a/WebStreamer/bot/__init__.py b/WebStreamer/bot/__init__.py new file mode 100644 index 0000000..e5ad8db --- /dev/null +++ b/WebStreamer/bot/__init__.py @@ -0,0 +1,12 @@ + +from pyrogram import Client +from ..vars import Var + +StreamBot = Client( + name='Web Streamer', + api_id=Var.API_ID, + api_hash=Var.API_HASH, + bot_token=Var.BOT_TOKEN, + sleep_threshold=Var.SLEEP_THRESHOLD, + workers=Var.WORKERS +) diff --git a/WebStreamer/bot/plugins/admin.py b/WebStreamer/bot/plugins/admin.py new file mode 100644 index 0000000..04372ec --- /dev/null +++ b/WebStreamer/bot/plugins/admin.py @@ -0,0 +1,90 @@ +# (c) @Avishkarpatil + +import os +import time +import string +import random +import asyncio +import aiofiles +import datetime +from WebStreamer.utils.broadcast_helper import send_msg +from WebStreamer.utils.database import Database +from WebStreamer.bot import StreamBot +from WebStreamer.vars import Var +from pyrogram import filters, Client +from pyrogram.types import Message +from pyrogram.enums.parse_mode import ParseMode +db = Database(Var.DATABASE_URL, Var.SESSION_NAME) +broadcast_ids = {} + + +@StreamBot.on_message(filters.command("status") & filters.private & filters.user(Var.OWNER_ID)) +async def sts(c: Client, m: Message): + total_users = await db.total_users_count() + await m.reply_text(text=f"**Total Users in DB:** `{total_users}`", parse_mode=ParseMode.MARKDOWN, quote=True) + + +@StreamBot.on_message(filters.command("broadcast") & filters.private & filters.user(Var.OWNER_ID) & filters.reply) +async def broadcast_(c, m): + all_users = await db.get_all_users() + broadcast_msg = m.reply_to_message + while True: + broadcast_id = ''.join([random.choice(string.ascii_letters) for i in range(3)]) + if not broadcast_ids.get(broadcast_id): + break + out = await m.reply_text( + text=f"Broadcast initiated! You will be notified with log file when all the users are notified." + ) + start_time = time.time() + total_users = await db.total_users_count() + done = 0 + failed = 0 + success = 0 + broadcast_ids[broadcast_id] = dict( + total=total_users, + current=done, + failed=failed, + success=success + ) + async with aiofiles.open('broadcast.txt', 'w') as broadcast_log_file: + async for user in all_users: + sts, msg = await send_msg( + user_id=int(user['id']), + message=broadcast_msg + ) + if msg is not None: + await broadcast_log_file.write(msg) + if sts == 200: + success += 1 + else: + failed += 1 + if sts == 400: + await db.delete_user(user['id']) + done += 1 + if broadcast_ids.get(broadcast_id) is None: + break + else: + broadcast_ids[broadcast_id].update( + dict( + current=done, + failed=failed, + success=success + ) + ) + if broadcast_ids.get(broadcast_id): + broadcast_ids.pop(broadcast_id) + completed_in = datetime.timedelta(seconds=int(time.time() - start_time)) + await asyncio.sleep(3) + await out.delete() + if failed == 0: + await m.reply_text( + text=f"broadcast completed in `{completed_in}`\n\nTotal users {total_users}.\nTotal done {done}, {success} success and {failed} failed.", + quote=True + ) + else: + await m.reply_document( + document='broadcast.txt', + caption=f"broadcast completed in `{completed_in}`\n\nTotal users {total_users}.\nTotal done {done}, {success} success and {failed} failed.", + quote=True + ) + os.remove('broadcast.txt') diff --git a/WebStreamer/bot/plugins/start.py b/WebStreamer/bot/plugins/start.py new file mode 100644 index 0000000..83198f8 --- /dev/null +++ b/WebStreamer/bot/plugins/start.py @@ -0,0 +1,261 @@ +import urllib.parse +from WebStreamer.bot import StreamBot +from WebStreamer.vars import Var +from WebStreamer.utils.human_readable import humanbytes +from WebStreamer.utils.database import Database +from pyrogram import filters +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram.errors import UserNotParticipant +from pyrogram.enums.parse_mode import ParseMode + +db = Database(Var.DATABASE_URL, Var.SESSION_NAME) + +START_TEXT = """ +👋 Hᴇʏ,{}\n +I'ᴍ Tᴇʟᴇɢʀᴀᴍ Fɪʟᴇs Sᴛʀᴇᴀᴍɪɴɢ Bᴏᴛ ᴀs ᴡᴇʟʟ Dɪʀᴇᴄᴛ Lɪɴᴋs Gᴇɴᴇʀᴀᴛᴇ\n +Cʟɪᴄᴋ ᴏɴ Hᴇʟᴘ ᴛᴏ ɢᴇᴛ ᴍᴏʀᴇ ɪɴғᴏʀᴍᴀᴛɪᴏɴ\n +𝗪𝗔𝗥𝗡𝗜𝗡𝗚 🚸 +🔞 Pʀᴏɴ ᴄᴏɴᴛᴇɴᴛꜱ ʟᴇᴀᴅꜱ ᴛᴏ ᴘᴇʀᴍᴀɴᴇɴᴛ ʙᴀɴ ʏᴏᴜ.\n\n +🍃 Bᴏᴛ Mᴀɪɴᴛᴀɪɴᴇᴅ Bʏ :@AvishkarPatil""" + +HELP_TEXT = """ +- Sᴇɴᴅ ᴍᴇ ᴀɴʏ ꜰɪʟᴇ (ᴏʀ) ᴍᴇᴅɪᴀ ꜰʀᴏᴍ ᴛᴇʟᴇɢʀᴀᴍ. +- I ᴡɪʟʟ ᴘʀᴏᴠɪᴅᴇ ᴇxᴛᴇʀɴᴀʟ ᴅɪʀᴇᴄᴛ ᴅᴏᴡɴʟᴏᴀᴅ ʟɪɴᴋ !. +- Aᴅᴅ Mᴇ ɪɴ ʏᴏᴜʀ Cʜᴀɴɴᴇʟ Fᴏʀ Dɪʀᴇᴄᴛ Dᴏᴡɴʟᴏᴀᴅ Lɪɴᴋs Bᴜᴛᴛᴏɴ +- Tʜɪs Pᴇʀᴍᴇᴀɴᴛ Lɪɴᴋ Wɪᴛʜ Fᴀsᴛᴇsᴛ Sᴘᴇᴇᴅ\n +🔸 𝗪𝗔𝗥𝗡𝗜𝗡𝗚 🚸\n +🔞 Pʀᴏɴ ᴄᴏɴᴛᴇɴᴛꜱ ʟᴇᴀᴅꜱ ᴛᴏ ᴘᴇʀᴍᴀɴᴇɴᴛ ʙᴀɴ ʏᴏᴜ.\n +Cᴏɴᴛᴀᴄᴛ ᴅᴇᴠᴇʟᴏᴘᴇʀ (ᴏʀ) ʀᴇᴘᴏʀᴛ ʙᴜɢꜱ : [ ᴄʟɪᴄᴋ ʜᴇʀᴇ ]""" + +ABOUT_TEXT = """ +⚜ Mʏ ɴᴀᴍᴇ : FileStreamX\n +🔸Vᴇʀꜱɪᴏɴ : 3.0.1\n +🔹Sᴏᴜʀᴄᴇ : Cʟɪᴄᴋ Hᴇʀᴇ\n +🔸GitHub : Fᴏʟʟᴏᴡ\n +🔹Dᴇᴠᴇʟᴏᴘᴇʀ : Aᴠɪsʜᴋᴀʀ Pᴀᴛɪʟ\n +🔸Lᴀꜱᴛ ᴜᴘᴅᴀᴛᴇᴅ : [ 26 - ᴊᴜɴᴇ - 2022 ] 03:35 ᴀᴍ""" + +START_BUTTONS = InlineKeyboardMarkup( + [[ + InlineKeyboardButton('Hᴇʟᴘ', callback_data='help'), + InlineKeyboardButton('Aʙᴏᴜᴛ', callback_data='about'), + InlineKeyboardButton('Cʟᴏsᴇ', callback_data='close') + ]] + ) +HELP_BUTTONS = InlineKeyboardMarkup( + [[ + InlineKeyboardButton('Hᴏᴍᴇ', callback_data='home'), + InlineKeyboardButton('Aʙᴏᴜᴛ', callback_data='about'), + InlineKeyboardButton('Cʟᴏsᴇ', callback_data='close') + ]] + ) +ABOUT_BUTTONS = InlineKeyboardMarkup( + [[ + InlineKeyboardButton('Hᴏᴍᴇ', callback_data='home'), + InlineKeyboardButton('Hᴇʟᴘ', callback_data='help'), + InlineKeyboardButton('Cʟᴏsᴇ', callback_data='close') + ]] + ) + +@StreamBot.on_callback_query() +async def cb_data(bot, update): + if update.data == "home": + await update.message.edit_text( + text=START_TEXT.format(update.from_user.mention), + disable_web_page_preview=True, + reply_markup=START_BUTTONS + ) + elif update.data == "help": + await update.message.edit_text( + text=HELP_TEXT, + disable_web_page_preview=True, + reply_markup=HELP_BUTTONS + ) + elif update.data == "about": + await update.message.edit_text( + text=ABOUT_TEXT, + disable_web_page_preview=True, + reply_markup=ABOUT_BUTTONS + ) + else: + await update.message.delete() + +def get_media_file_size(m): + media = m.video or m.audio or m.document + if media and media.file_size: + return media.file_size + else: + return None + + +def get_media_file_name(m): + media = m.video or m.document or m.audio + if media and media.file_name: + return urllib.parse.quote_plus(media.file_name) + else: + return None + + +@StreamBot.on_message(filters.command('start') & filters.private) +async def start(b, m): + if not await db.is_user_exist(m.from_user.id): + await db.add_user(m.from_user.id) + await b.send_message( + Var.BIN_CHANNEL, + f"**Nᴇᴡ Usᴇʀ Jᴏɪɴᴇᴅ:** \n\n__Mʏ Nᴇᴡ Fʀɪᴇɴᴅ__ [{m.from_user.first_name}](tg://user?id={m.from_user.id}) __Sᴛᴀʀᴛᴇᴅ Yᴏᴜʀ Bᴏᴛ !!__" + ) + usr_cmd = m.text.split("_")[-1] + if usr_cmd == "/start": + if Var.UPDATES_CHANNEL != "None": + try: + user = await b.get_chat_member(Var.UPDATES_CHANNEL, m.chat.id) + if user.status == "kicked": + await b.send_message( + chat_id=m.chat.id, + text="__Sᴏʀʀʏ Sɪʀ, Yᴏᴜ ᴀʀᴇ Bᴀɴɴᴇᴅ ᴛᴏ ᴜsᴇ ᴍᴇ. Cᴏɴᴛᴀᴄᴛ ᴛʜᴇ Dᴇᴠᴇʟᴏᴘᴇʀ__\n\n @AvishkarPatil **Tʜᴇʏ Wɪʟʟ Hᴇʟᴘ Yᴏᴜ**", + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True + ) + return + except UserNotParticipant: + await b.send_message( + chat_id=m.chat.id, + text="Jᴏɪɴ ᴍʏ ᴜᴘᴅᴀᴛᴇ ᴄʜᴀɴɴᴇʟ ᴛᴏ ᴜsᴇ ᴍᴇ 🔐", + reply_markup=InlineKeyboardMarkup( + [[ + InlineKeyboardButton("Jᴏɪɴ ɴᴏᴡ 🔓", url=f"https://t.me/{Var.UPDATES_CHANNEL}") + ]] + ), + parse_mode=ParseMode.HTML + ) + return + except Exception: + await b.send_message( + chat_id=m.chat.id, + text="Sᴏᴍᴇᴛʜɪɴɢ ᴡʀᴏɴɢ ᴄᴏɴᴛᴀᴄᴛ ᴍʏ ᴅᴇᴠᴇʟᴏᴘᴇʀ [ ᴄʟɪᴄᴋ ʜᴇʀᴇ ]", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True) + return + await m.reply_text( + text=START_TEXT.format(m.from_user.mention), + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + reply_markup=START_BUTTONS + ) + + + else: + if Var.UPDATES_CHANNEL != "None": + try: + user = await b.get_chat_member(Var.UPDATES_CHANNEL, m.chat.id) + if user.status == "kicked": + await b.send_message( + chat_id=m.chat.id, + text="**Sᴏʀʀʏ Sɪʀ, Yᴏᴜ ᴀʀᴇ Bᴀɴɴᴇᴅ ᴛᴏ ᴜsᴇ ᴍᴇ. Qᴜɪᴄᴋʟʏ ᴄᴏɴᴛᴀᴄᴛ** @Avishkarpatil", + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True + ) + return + except UserNotParticipant: + await b.send_message( + chat_id=m.chat.id, + text="**Pʟᴇᴀsᴇ Jᴏɪɴ Mʏ Uᴘᴅᴀᴛᴇs Cʜᴀɴɴᴇʟ ᴛᴏ ᴜsᴇ ᴛʜɪs Bᴏᴛ**!\n\n**Dᴜᴇ ᴛᴏ Oᴠᴇʀʟᴏᴀᴅ, Oɴʟʏ Cʜᴀɴɴᴇʟ Sᴜʙsᴄʀɪʙᴇʀs ᴄᴀɴ ᴜsᴇ ᴛʜᴇ Bᴏᴛ**!", + reply_markup=InlineKeyboardMarkup( + [[ + InlineKeyboardButton("🤖 Jᴏɪɴ Uᴘᴅᴀᴛᴇs Cʜᴀɴɴᴇʟ", url=f"https://t.me/{Var.UPDATES_CHANNEL}")], + [InlineKeyboardButton("🔄 Refresh / Try Again", url=f"https://t.me/{(await b.get_me()).username}?start=AvishkarPatil_{usr_cmd}") + + ]] + ), + parse_mode=ParseMode.MARKDOWN + ) + return + except Exception: + await b.send_message( + chat_id=m.chat.id, + text="**Sᴏᴍᴇᴛʜɪɴɢ ᴡᴇɴᴛ Wʀᴏɴɢ. Cᴏɴᴛᴀᴄᴛ ᴍᴇ** [Aᴠɪsʜᴋᴀʀ Pᴀᴛɪʟ](https://t.me/Avishkarpatil).", + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True) + return + + get_msg = await b.get_messages(chat_id=Var.BIN_CHANNEL, message_ids=int(usr_cmd)) + file_name = get_media_file_name(get_msg) + file_size = humanbytes(get_media_file_size(get_msg)) + + stream_link = "https://{}/{}/{}".format(Var.FQDN, get_msg.id, file_name) if Var.ON_HEROKU or Var.NO_PORT else \ + "http://{}:{}/{}/{}".format(Var.FQDN, + Var.PORT, + get_msg.id, + file_name) + + msg_text =""" +𝗬𝗼𝘂𝗿 𝗟𝗶𝗻𝗸 𝗚𝗲𝗻𝗲𝗿𝗮𝘁𝗲𝗱 !\n +📂 Fɪʟᴇ ɴᴀᴍᴇ : {}\n +📦 Fɪʟᴇ ꜱɪᴢᴇ : {}\n +📥 Dᴏᴡɴʟᴏᴀᴅ : {}\n +🚸 Nᴏᴛᴇ : Lɪɴᴋ ᴇxᴘɪʀᴇᴅ ɪɴ 24 ʜᴏᴜʀꜱ\n +🍃 Bᴏᴛ Mᴀɪɴᴛᴀɪɴᴇᴅ Bʏ : @AvishkarPatil +""" + + await m.reply_text( + text=msg_text.format(file_name, file_size, stream_link), + parse_mode=ParseMode.HTML, + reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Dᴏᴡɴʟᴏᴀᴅ ɴᴏᴡ 📥", url=stream_link)]]) + ) + + + +@StreamBot.on_message(filters.private & filters.command(["about"])) +async def start(bot, update): + await update.reply_text( + text=ABOUT_TEXT.format(update.from_user.mention), + disable_web_page_preview=True, + reply_markup=ABOUT_BUTTONS + ) + + +@StreamBot.on_message(filters.command('help') & filters.private) +async def help_handler(bot, message): + if not await db.is_user_exist(message.from_user.id): + await db.add_user(message.from_user.id) + await bot.send_message( + Var.BIN_CHANNEL, + f"**Nᴇᴡ Usᴇʀ Jᴏɪɴᴇᴅ **\n\n__Mʏ Nᴇᴡ Fʀɪᴇɴᴅ__ [{message.from_user.first_name}](tg://user?id={message.from_user.id}) __Started Your Bot !!__" + ) + if Var.UPDATES_CHANNEL is not None: + try: + user = await bot.get_chat_member(Var.UPDATES_CHANNEL, message.chat.id) + if user.status == "kicked": + await bot.send_message( + chat_id=message.chat.id, + text="Sᴏʀʀʏ Sɪʀ, Yᴏᴜ ᴀʀᴇ Bᴀɴɴᴇᴅ ᴛᴏ ᴜsᴇ ᴍᴇ. Cᴏɴᴛᴀᴄᴛ ᴛʜᴇ Dᴇᴠᴇʟᴏᴘᴇʀ", + parse_mode=ParseMode.HTML, + disable_web_page_preview=True + ) + return + except UserNotParticipant: + await bot.send_message( + chat_id=message.chat.id, + text="**Pʟᴇᴀsᴇ Jᴏɪɴ Mʏ Uᴘᴅᴀᴛᴇs Cʜᴀɴɴᴇʟ ᴛᴏ ᴜsᴇ ᴛʜɪs Bᴏᴛ!**\n\n__Dᴜᴇ ᴛᴏ Oᴠᴇʀʟᴏᴀᴅ, Oɴʟʏ Cʜᴀɴɴᴇʟ Sᴜʙsᴄʀɪʙᴇʀs ᴄᴀɴ ᴜsᴇ ᴛʜᴇ Bᴏᴛ!__", + reply_markup=InlineKeyboardMarkup( + [[ + InlineKeyboardButton("🤖 Jᴏɪɴ Uᴘᴅᴀᴛᴇs Cʜᴀɴɴᴇʟ", url=f"https://t.me/{Var.UPDATES_CHANNEL}") + ]] + ), + parse_mode=ParseMode.MARKDOWN + ) + return + except Exception: + await bot.send_message( + chat_id=message.chat.id, + text="__Sᴏᴍᴇᴛʜɪɴɢ ᴡᴇɴᴛ Wʀᴏɴɢ. Cᴏɴᴛᴀᴄᴛ ᴍᴇ__ [Aᴠɪsʜᴋᴀʀ Pᴀᴛɪʟ](https://t.me/Avishkarpatil).", + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True) + return + await message.reply_text( + text=HELP_TEXT, + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + reply_markup=HELP_BUTTONS + ) + diff --git a/WebStreamer/bot/plugins/stream.py b/WebStreamer/bot/plugins/stream.py new file mode 100644 index 0000000..e374e21 --- /dev/null +++ b/WebStreamer/bot/plugins/stream.py @@ -0,0 +1,134 @@ + +# (c) @Avishkarpatil + + +import asyncio +import urllib.parse +from WebStreamer.bot import StreamBot +from WebStreamer.utils.database import Database +from WebStreamer.utils.human_readable import humanbytes +from WebStreamer.vars import Var +from pyrogram import filters, Client +from pyrogram.errors import FloodWait, UserNotParticipant +from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram.enums.parse_mode import ParseMode +db = Database(Var.DATABASE_URL, Var.SESSION_NAME) + + +def get_media_file_size(m): + media = m.video or m.audio or m.document + if media and media.file_size: + return media.file_size + else: + return None + + +def get_media_file_name(m): + media = m.video or m.document or m.audio + if media and media.file_name: + return urllib.parse.quote_plus(media.file_name) + else: + return None + + +@StreamBot.on_message(filters.private & (filters.document | filters.video | filters.audio), group=4) +async def private_receive_handler(c: Client, m: Message): + if not await db.is_user_exist(m.from_user.id): + await db.add_user(m.from_user.id) + await c.send_message( + Var.BIN_CHANNEL, + f"Nᴇᴡ Usᴇʀ Jᴏɪɴᴇᴅ : \n\nNᴀᴍᴇ : [{m.from_user.first_name}](tg://user?id={m.from_user.id}) Sᴛᴀʀᴛᴇᴅ Yᴏᴜʀ Bᴏᴛ !!" + ) + if Var.UPDATES_CHANNEL != "None": + try: + user = await c.get_chat_member(Var.UPDATES_CHANNEL, m.chat.id) + if user.status == "kicked": + await c.send_message( + chat_id=m.chat.id, + text="__Sᴏʀʀʏ Sɪʀ, Yᴏᴜ ᴀʀᴇ Bᴀɴɴᴇᴅ ᴛᴏ ᴜsᴇ ᴍᴇ.__\n\n **Cᴏɴᴛᴀᴄᴛ Dᴇᴠᴇʟᴏᴘᴇʀ @Avishkarpatil Tʜᴇʏ Wɪʟʟ Hᴇʟᴘ Yᴏᴜ**", + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True + ) + return + except UserNotParticipant: + await c.send_message( + chat_id=m.chat.id, + text="""Jᴏɪɴ ᴍʏ ᴜᴘᴅᴀᴛᴇ ᴄʜᴀɴɴᴇʟ ᴛᴏ ᴜꜱᴇ ᴍᴇ 🔐""", + reply_markup=InlineKeyboardMarkup( + [[ InlineKeyboardButton("Jᴏɪɴ ɴᴏᴡ 🔓", url=f"https://t.me/{Var.UPDATES_CHANNEL}") ]] + ), + parse_mode=ParseMode.HTML + ) + return + except Exception: + await c.send_message( + chat_id=m.chat.id, + text="**Sᴏᴍᴇᴛʜɪɴɢ ᴡᴇɴᴛ Wʀᴏɴɢ. Cᴏɴᴛᴀᴄᴛ ᴍʏ ʙᴏss** @Avishkarpatil", + parse_mode=ParseMode.MARKDOWN, + disable_web_page_preview=True) + return + try: + log_msg = await m.forward(chat_id=Var.BIN_CHANNEL) + file_name = get_media_file_name(m) + file_size = humanbytes(get_media_file_size(m)) + stream_link = "https://{}/{}/{}".format(Var.FQDN, log_msg.id, file_name) if Var.ON_HEROKU or Var.NO_PORT else \ + "http://{}:{}/{}/{}".format(Var.FQDN, + Var.PORT, + log_msg.id, + file_name) + + msg_text =""" +𝗬𝗼𝘂𝗿 𝗟𝗶𝗻𝗸 𝗚𝗲𝗻𝗲𝗿𝗮𝘁𝗲𝗱 !\n +📂 Fɪʟᴇ ɴᴀᴍᴇ : {}\n +📦 Fɪʟᴇ ꜱɪᴢᴇ : {}\n +📥 Dᴏᴡɴʟᴏᴀᴅ : {}\n +🚸 Nᴏᴛᴇ : Tʜɪs ᴘᴇʀᴍᴀɴᴇɴᴛ Lɪɴᴋ, Nᴏᴛ Exᴘɪʀᴇᴅ\n +© @AvishkarPatil """ + + await log_msg.reply_text(text=f"**RᴇQᴜᴇꜱᴛᴇᴅ ʙʏ :** [{m.from_user.first_name}](tg://user?id={m.from_user.id})\n**Uꜱᴇʀ ɪᴅ :** `{m.from_user.id}`\n**Dᴏᴡɴʟᴏᴀᴅ ʟɪɴᴋ :** {stream_link}", disable_web_page_preview=True, parse_mode=ParseMode.MARKDOWN, quote=True) + await m.reply_text( + text=msg_text.format(file_name, file_size, stream_link), + parse_mode=ParseMode.HTML, + disable_web_page_preview=True, + reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("Dᴏᴡɴʟᴏᴀᴅ ɴᴏᴡ 📥", url=stream_link)]]), + quote=True + ) + except FloodWait as e: + print(f"Sleeping for {str(e.x)}s") + await asyncio.sleep(e.x) + await c.send_message(chat_id=Var.BIN_CHANNEL, text=f"Gᴏᴛ FʟᴏᴏᴅWᴀɪᴛ ᴏғ {str(e.x)}s from [{m.from_user.first_name}](tg://user?id={m.from_user.id})\n\n**𝚄𝚜𝚎𝚛 𝙸𝙳 :** `{str(m.from_user.id)}`", disable_web_page_preview=True, parse_mode=ParseMode.MARKDOWN) + + +@StreamBot.on_message(filters.channel & (filters.document | filters.video), group=-1) +async def channel_receive_handler(bot, broadcast): + if int(broadcast.chat.id) in Var.BANNED_CHANNELS: + await bot.leave_chat(broadcast.chat.id) + return + try: + log_msg = await broadcast.forward(chat_id=Var.BIN_CHANNEL) + stream_link = "https://{}/{}".format(Var.FQDN, log_msg.id) if Var.ON_HEROKU or Var.NO_PORT else \ + "http://{}:{}/{}".format(Var.FQDN, + Var.PORT, + log_msg.id) + await log_msg.reply_text( + text=f"**Cʜᴀɴɴᴇʟ Nᴀᴍᴇ:** `{broadcast.chat.title}`\n**Cʜᴀɴɴᴇʟ ID:** `{broadcast.chat.id}`\n**Rᴇǫᴜᴇsᴛ ᴜʀʟ:** https://t.me/{(await bot.get_me()).username}?start=AvishkarPatil_{str(log_msg.id)}", + # text=f"**Cʜᴀɴɴᴇʟ Nᴀᴍᴇ:** `{broadcast.chat.title}`\n**Cʜᴀɴɴᴇʟ ID:** `{broadcast.chat.id}`\n**Rᴇǫᴜᴇsᴛ ᴜʀʟ:** https://t.me/FxStreamBot?start=AvishkarPatil_{str(log_msg.id)}", + quote=True, + parse_mode=ParseMode.MARKDOWN + ) + await bot.edit_message_reply_markup( + chat_id=broadcast.chat.id, + message_id=broadcast.id, + reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton("Dᴏᴡɴʟᴏᴀᴅ ʟɪɴᴋ 📥", url=f"https://t.me/{(await bot.get_me()).username}?start=AvishkarPatil_{str(log_msg.id)}")]]) + # [[InlineKeyboardButton("Dᴏᴡɴʟᴏᴀᴅ ʟɪɴᴋ 📥", url=f"https://t.me/FxStreamBot?start=AvishkarPatil_{str(log_msg.id)}")]]) + ) + except FloodWait as w: + print(f"Sleeping for {str(w.x)}s") + await asyncio.sleep(w.x) + await bot.send_message(chat_id=Var.BIN_CHANNEL, + text=f"Gᴏᴛ FʟᴏᴏᴅWᴀɪᴛ ᴏғ {str(w.x)}s from {broadcast.chat.title}\n\n**Cʜᴀɴɴᴇʟ ID:** `{str(broadcast.chat.id)}`", + disable_web_page_preview=True, parse_mode=ParseMode.MARKDOWN) + except Exception as e: + await bot.send_message(chat_id=Var.BIN_CHANNEL, text=f"**#ᴇʀʀᴏʀ_ᴛʀᴀᴄᴇʙᴀᴄᴋ:** `{e}`", disable_web_page_preview=True, parse_mode=ParseMode.MARKDOWN) + print(f"Cᴀɴ'ᴛ Eᴅɪᴛ Bʀᴏᴀᴅᴄᴀsᴛ Mᴇssᴀɢᴇ!\nEʀʀᴏʀ: {e}") diff --git a/WebStreamer/server/__init__.py b/WebStreamer/server/__init__.py new file mode 100644 index 0000000..a344be9 --- /dev/null +++ b/WebStreamer/server/__init__.py @@ -0,0 +1,9 @@ + +from aiohttp import web +from .stream_routes import routes + + +async def web_server(): + web_app = web.Application(client_max_size=30000000) + web_app.add_routes(routes) + return web_app diff --git a/WebStreamer/server/stream_routes.py b/WebStreamer/server/stream_routes.py new file mode 100644 index 0000000..e3a4638 --- /dev/null +++ b/WebStreamer/server/stream_routes.py @@ -0,0 +1,79 @@ +# Avishkar Patil | AbirHasan2005 + +import math +import logging +import secrets +import mimetypes +from ..vars import Var +from aiohttp import web +from ..bot import StreamBot +from ..utils.custom_dl import TGCustomYield, chunk_size, offset_fix + +routes = web.RouteTableDef() + + +@routes.get("/", allow_head=True) +async def root_route_handler(request): + bot_details = await StreamBot.get_me() + return web.json_response({"status": "running", + "maintained_by": "Avishkar_Patil", + "server_permission": "Open", + "Telegram_Bot": '@'+bot_details.username}) + + +@routes.get("/{message_id}") +@routes.get("/{message_id}/") +@routes.get(r"/{message_id:\d+}/{name}") +async def stream_handler(request): + try: + message_id = int(request.match_info['message_id']) + return await media_streamer(request, message_id) + except ValueError as e: + logging.error(e) + raise web.HTTPNotFound + + +async def media_streamer(request, message_id: int): + range_header = request.headers.get('Range', 0) + media_msg = await StreamBot.get_messages(Var.BIN_CHANNEL, message_id) + file_properties = await TGCustomYield().generate_file_properties(media_msg) + file_size = file_properties.file_size + + 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 = request.http_range.start or 0 + until_bytes = request.http_range.stop or file_size - 1 + + req_length = until_bytes - from_bytes + + new_chunk_size = await chunk_size(req_length) + offset = await offset_fix(from_bytes, new_chunk_size) + first_part_cut = from_bytes - offset + last_part_cut = (until_bytes % new_chunk_size) + 1 + part_count = math.ceil(req_length / new_chunk_size) + body = TGCustomYield().yield_file(media_msg, offset, first_part_cut, last_part_cut, part_count, + new_chunk_size) + + file_name = file_properties.file_name if file_properties.file_name \ + else f"{secrets.token_hex(2)}.jpeg" + mime_type = file_properties.mime_type if file_properties.mime_type \ + else f"{mimetypes.guess_type(file_name)}" + + return_resp = web.Response( + status=206 if range_header else 200, + body=body, + headers={ + "Content-Type": mime_type, + "Content-Range": f"bytes {from_bytes}-{until_bytes}/{file_size}", + "Content-Disposition": f'attachment; filename="{file_name}"', + "Accept-Ranges": "bytes", + } + ) + + if return_resp.status == 200: + return_resp.headers.add("Content-Length", str(file_size)) + + return return_resp diff --git a/WebStreamer/utils/__init__.py b/WebStreamer/utils/__init__.py new file mode 100644 index 0000000..ff0636f --- /dev/null +++ b/WebStreamer/utils/__init__.py @@ -0,0 +1 @@ +# This file is a part of avipatilpro/FileStreamBot diff --git a/WebStreamer/utils/broadcast_helper.py b/WebStreamer/utils/broadcast_helper.py new file mode 100644 index 0000000..b629741 --- /dev/null +++ b/WebStreamer/utils/broadcast_helper.py @@ -0,0 +1,21 @@ + +import asyncio +import traceback +from pyrogram.errors import FloodWait, InputUserDeactivated, UserIsBlocked, PeerIdInvalid + + +async def send_msg(user_id, message): + try: + await message.forward(chat_id=user_id) + return 200, None + except FloodWait as e: + await asyncio.sleep(e.x) + return send_msg(user_id, message) + except InputUserDeactivated: + return 400, f"{user_id} : deactivated\n" + except UserIsBlocked: + return 400, f"{user_id} : blocked the bot\n" + except PeerIdInvalid: + return 400, f"{user_id} : user id invalid\n" + except Exception as e: + return 500, f"{user_id} : {traceback.format_exc()}\n" diff --git a/WebStreamer/utils/custom_dl.py b/WebStreamer/utils/custom_dl.py new file mode 100644 index 0000000..b375838 --- /dev/null +++ b/WebStreamer/utils/custom_dl.py @@ -0,0 +1,231 @@ + +import math +from typing import Union +from pyrogram.types import Message +from ..bot import StreamBot +from pyrogram import Client, utils, raw +from pyrogram.session import Session, Auth +from pyrogram.errors import AuthBytesInvalid +from pyrogram.file_id import FileId, FileType, ThumbnailSource + + +async def chunk_size(length): + return 2 ** max(min(math.ceil(math.log2(length / 1024)), 10), 2) * 1024 + + +async def offset_fix(offset, chunksize): + offset -= offset % chunksize + return offset + + +class TGCustomYield: + def __init__(self): + """ A custom method to stream files from telegram. + functions: + generate_file_properties: returns the properties for a media on a specific message contained in FileId class. + generate_media_session: returns the media session for the DC that contains the media file on the message. + yield_file: yield a file from telegram servers for streaming. + """ + self.main_bot = StreamBot + + @staticmethod + async def generate_file_properties(msg: Message): + error_message = "This message doesn't contain any downloadable media" + available_media = ("audio", "document", "photo", "sticker", "animation", "video", "voice", "video_note") + + if isinstance(msg, Message): + for kind in available_media: + media = getattr(msg, kind, None) + + if media is not None: + break + else: + raise ValueError(error_message) + else: + media = msg + + if isinstance(media, str): + file_id_str = media + else: + file_id_str = media.file_id + + file_id_obj = FileId.decode(file_id_str) + + # The below lines are added to avoid a break in routes.py + setattr(file_id_obj, "file_size", getattr(media, "file_size", 0)) + setattr(file_id_obj, "mime_type", getattr(media, "mime_type", "")) + setattr(file_id_obj, "file_name", getattr(media, "file_name", "")) + + return file_id_obj + + async def generate_media_session(self, client: Client, msg: Message): + data = await self.generate_file_properties(msg) + + media_session = client.media_sessions.get(data.dc_id, None) + + if media_session is None: + if data.dc_id != await client.storage.dc_id(): + media_session = Session( + client, data.dc_id, await Auth(client, data.dc_id, await client.storage.test_mode()).create(), + await client.storage.test_mode(), is_media=True + ) + await media_session.start() + + for _ in range(3): + exported_auth = await client.invoke( + raw.functions.auth.ExportAuthorization( + dc_id=data.dc_id + ) + ) + + try: + await media_session.invoke( + raw.functions.auth.ImportAuthorization( + id=exported_auth.id, + bytes=exported_auth.bytes + ) + ) + except AuthBytesInvalid: + continue + else: + break + else: + await media_session.stop() + raise AuthBytesInvalid + else: + media_session = Session( + client, data.dc_id, await client.storage.auth_key(), + await client.storage.test_mode(), is_media=True + ) + await media_session.start() + + client.media_sessions[data.dc_id] = media_session + + return media_session + + @staticmethod + async def get_location(file_id: FileId): + file_type = file_id.file_type + + if file_type == FileType.CHAT_PHOTO: + if file_id.chat_id > 0: + peer = raw.types.InputPeerUser( + user_id=file_id.chat_id, + access_hash=file_id.chat_access_hash + ) + else: + if file_id.chat_access_hash == 0: + peer = raw.types.InputPeerChat( + chat_id=-file_id.chat_id + ) + else: + peer = raw.types.InputPeerChannel( + channel_id=utils.get_channel_id(file_id.chat_id), + access_hash=file_id.chat_access_hash + ) + + location = raw.types.InputPeerPhotoFileLocation( + peer=peer, + volume_id=file_id.volume_id, + local_id=file_id.local_id, + big=file_id.thumbnail_source == ThumbnailSource.CHAT_PHOTO_BIG + ) + elif file_type == FileType.PHOTO: + location = raw.types.InputPhotoFileLocation( + id=file_id.media_id, + access_hash=file_id.access_hash, + file_reference=file_id.file_reference, + thumb_size=file_id.thumbnail_size + ) + else: + location = raw.types.InputDocumentFileLocation( + id=file_id.media_id, + access_hash=file_id.access_hash, + file_reference=file_id.file_reference, + thumb_size=file_id.thumbnail_size + ) + + return location + + async def yield_file(self, media_msg: Message, offset: int, first_part_cut: int, + last_part_cut: int, part_count: int, chunk_size: int) -> Union[str, None]: #pylint: disable=unsubscriptable-object + client = self.main_bot + data = await self.generate_file_properties(media_msg) + media_session = await self.generate_media_session(client, media_msg) + + current_part = 1 + + location = await self.get_location(data) + + r = await media_session.invoke( + raw.functions.upload.GetFile( + location=location, + offset=offset, + limit=chunk_size + ), + ) + + if isinstance(r, raw.types.upload.File): + while current_part <= part_count: + chunk = r.bytes + if not chunk: + break + offset += chunk_size + if part_count == 1: + yield chunk[first_part_cut:last_part_cut] + break + if current_part == 1: + yield chunk[first_part_cut:] + if 1 < current_part <= part_count: + yield chunk + + r = await media_session.invoke( + raw.functions.upload.GetFile( + location=location, + offset=offset, + limit=chunk_size + ), + ) + + current_part += 1 + + async def download_as_bytesio(self, media_msg: Message): + client = self.main_bot + data = await self.generate_file_properties(media_msg) + media_session = await self.generate_media_session(client, media_msg) + + location = await self.get_location(data) + + limit = 1024 * 1024 + offset = 0 + + r = await media_session.invoke( + raw.functions.upload.GetFile( + location=location, + offset=offset, + limit=limit + ) + ) + + if isinstance(r, raw.types.upload.File): + m_file = [] + # m_file.name = file_name + while True: + chunk = r.bytes + + if not chunk: + break + + m_file.append(chunk) + + offset += limit + + r = await media_session.invoke( + raw.functions.upload.GetFile( + location=location, + offset=offset, + limit=limit + ) + ) + + return m_file diff --git a/WebStreamer/utils/database.py b/WebStreamer/utils/database.py new file mode 100644 index 0000000..cf72f8d --- /dev/null +++ b/WebStreamer/utils/database.py @@ -0,0 +1,36 @@ + + +import datetime +import motor.motor_asyncio + + +class Database: + def __init__(self, uri, database_name): + self._client = motor.motor_asyncio.AsyncIOMotorClient(uri) + self.db = self._client[database_name] + self.col = self.db.users + + def new_user(self, id): + return dict( + id=id, + join_date=datetime.date.today().isoformat() + ) + + async def add_user(self, id): + user = self.new_user(id) + await self.col.insert_one(user) + + async def is_user_exist(self, id): + user = await self.col.find_one({'id': int(id)}) + return True if user else False + + async def total_users_count(self): + count = await self.col.count_documents({}) + return count + + async def get_all_users(self): + all_users = self.col.find({}) + return all_users + + async def delete_user(self, user_id): + await self.col.delete_many({'id': int(user_id)}) diff --git a/WebStreamer/utils/human_readable.py b/WebStreamer/utils/human_readable.py new file mode 100644 index 0000000..c7f2fdb --- /dev/null +++ b/WebStreamer/utils/human_readable.py @@ -0,0 +1,13 @@ + +def humanbytes(size): + # https://stackoverflow.com/a/49361727/4723940 + # 2**10 = 1024 + if not size: + return "" + power = 2**10 + n = 0 + Dic_powerN = {0: ' ', 1: 'Ki', 2: 'Mi', 3: 'Gi', 4: 'Ti'} + while size > power: + size /= power + n += 1 + return str(round(size, 2)) + " " + Dic_powerN[n] + 'B' diff --git a/WebStreamer/utils/keepalive.py b/WebStreamer/utils/keepalive.py new file mode 100644 index 0000000..f6601a8 --- /dev/null +++ b/WebStreamer/utils/keepalive.py @@ -0,0 +1,18 @@ +import asyncio +import logging +import aiohttp +import traceback +from WebStreamer.vars import Var + +async def ping_server(): + sleep_time = Var.PING_INTERVAL + while True: + await asyncio.sleep(sleep_time) + try: + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session: + async with session.get(Var.URL) as resp: + logging.info("Pinged server with response: {}".format(resp.status)) + except TimeoutError: + logging.warning("Couldn't connect to the site URL..!") + except Exception: + traceback.print_exc() diff --git a/WebStreamer/vars.py b/WebStreamer/vars.py new file mode 100644 index 0000000..130c47f --- /dev/null +++ b/WebStreamer/vars.py @@ -0,0 +1,33 @@ +# (c) @AvishkarPatil | @EverythingSuckz + +from os import getenv, environ +from dotenv import load_dotenv + +load_dotenv() + + +class Var(object): + API_ID = int(getenv('API_ID')) + API_HASH = str(getenv('API_HASH')) + BOT_TOKEN = str(getenv('BOT_TOKEN')) + SESSION_NAME = str(getenv('SESSION_NAME', 'AviStreamBot')) + SLEEP_THRESHOLD = int(getenv('SLEEP_THRESHOLD', '60')) + WORKERS = int(getenv('WORKERS', '4')) + BIN_CHANNEL = int(getenv('BIN_CHANNEL')) + PORT = int(getenv('PORT', 8080)) + BIND_ADRESS = str(getenv('WEB_SERVER_BIND_ADDRESS', '0.0.0.0')) + OWNER_ID = int(getenv('OWNER_ID', '797848243')) + NO_PORT = bool(getenv('NO_PORT', False)) + APP_NAME = None + if 'DYNO' in environ: + ON_HEROKU = True + APP_NAME = str(getenv('APP_NAME')) + else: + ON_HEROKU = False + FQDN = str(getenv('FQDN', BIND_ADRESS)) if not ON_HEROKU or getenv('FQDN') else APP_NAME+'.herokuapp.com' + URL = "https://{}/".format(FQDN) if ON_HEROKU or NO_PORT else \ + "http://{}:{}/".format(FQDN, PORT) + DATABASE_URL = str(getenv('DATABASE_URL')) + PING_INTERVAL = int(getenv('PING_INTERVAL', '500')) + UPDATES_CHANNEL = str(getenv('UPDATES_CHANNEL', None)) + BANNED_CHANNELS = list(set(int(x) for x in str(getenv("BANNED_CHANNELS", "-1001296894100")).split())) diff --git a/app.json b/app.json new file mode 100644 index 0000000..8054b3b --- /dev/null +++ b/app.json @@ -0,0 +1,98 @@ +{ + "name": "Avi-FileStreamBot", + "description": "A Pyrogram Telegram bot to Stream Telegram files to web. @Avishkarpatil", + "keywords": [ + "telegram", + "stream", + "web", + "pyrogram", + "aiohttp", + "python", + "plugin", + "modular", + "media" + ], + "repository": "https://github.com/avipatilpro/FileStreamBot/", + "success_url": "/", + "logo": "https://i.ibb.co/ZJzJ9Hq/link-3x.png", + "website": "avipatilweb.ml", + "env": { + "ENV": { + "description": "Set this to True if you don't want to crash the bot", + "value": "True" + }, + "APP_NAME": { + "description": "Copy-Paste the app name that you just typed above." + }, + "API_ID": { + "description": "Get this value from https://my.telegram.org" + }, + "API_HASH": { + "description": "Get this value from https://my.telegram.org" + }, + "BOT_TOKEN": { + "description": "Get this value from @BotFather" + }, + "BIN_CHANNEL": { + "description": "The BIN Channel ID. Read the readme for more info about this var" + + }, + "PING_INTERVAL": { + "description": "Add Ping Interval Default Is 500", + "required": false + }, + "DATABASE_URL": { + "description": "MongoDB URI for saving User IDs when they first Start the Bot. We will use that for Broadcasting to them. I will try to add more features related with Database. If you need help to get the URI you can ask in Support Group: https://t.me/linux_repo" + }, + "OWNER_ID": { + "description": "Your Telegram User ID" + }, + "BANNED_CHANNELS": { + "description": "Put IDs of Banned Channels where bot will not work. You can add multiple IDs & separate with Space.", + "required": false + }, + "UPDATES_CHANNEL": { + "description": "Put a Public Channel Username, so every user have to Join that channel to use the bot. Must add bot to channel as Admin to work properly.", + "required": false + + }, + "SLEEP_THRESHOLD": { + "description": "Floodwait Sleep timer. Read the readme for more info about this var", + "required": false + }, + "WORKERS": { + "description": "No. of workers that is to be assigned. Read the readme for more info about this var", + "required": false + }, + "PORT": { + "description": "Port that you want your webapp to be listened to. Read the readme for more info about this var", + "required": false + }, + "NO_PORT": { + "description": "If you don't want your port to be displayed. Read the readme for more info about this var", + "value": "False", + "required": false + }, + "BIND_ADRESS": { + "description": "Read the readme for more info about this var", + "required": false + }, + "FQDN": { + "description": "Read the readme for more info about this var", + "required": false + }, + "SESSION_NAME": { + "description": " Session Name for Bot [ Add AvishkarPatil ]", + "required": false + } + }, + "buildpacks": [{ + "url": "heroku/python" + }], + "formation": { + "web": { + "quantity": 1, + "size": "free" + } + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6460be1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +pyrogram>=2.0.0,<=2.0.30 +tgcrypto<=1.2.3 +aiohttp<=3.8.1 +python-dotenv<=0.20.0 +motor +aiofiles +dnspython