Dr.Caduceus commited on
Commit
af2b875
·
unverified ·
0 Parent(s):

Initial commit.

Browse files
Dockerfile ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+
3
+ WORKDIR /app
4
+ COPY . /app
5
+
6
+ RUN pip install -r requirements.txt
7
+
8
+ CMD ["python", "-m", "bot"]
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: python -m bot
README.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center"><h1>🌐File Stream Bot</h1>
2
+ <b>An open-source Python Telegram bot to transmit Telegram files over HTTP.</b>
3
+
4
+ <a href="https://t.me/DrFileStreamBot"><b>Demo Bot</b></a>
5
+ </div><br>
6
+
7
+ ## **📑 INDEX**
8
+
9
+ * [**⚙️ Installation**](#installation)
10
+ * [Python & Git](#i-1)
11
+ * [Download](#i-2)
12
+ * [Requirements](#i-3)
13
+ * [**📝 Variables**](#variables)
14
+ * [**🕹 Deployment**](#deployment)
15
+ * [Locally](#d-1)
16
+ * [Docker](#d-2)
17
+ * [**⛑️ Need help!**](#help)
18
+ * [**❤️ Credits & Thanks**](#credits)
19
+
20
+ <a name="installation"></a>
21
+
22
+ ## ⚙️ Installation
23
+
24
+ <a name="i-1"></a>
25
+
26
+ **1.Install Python & Git:**
27
+
28
+ For Windows:
29
+ ```
30
+ winget install Python.Python.3.11
31
+ winget install Git.Git
32
+ ```
33
+ For Linux:
34
+ ```
35
+ sudo apt-get update && sudo apt-get install -y python3.11 git pip
36
+ ```
37
+ For macOS:
38
+ ```
39
+ brew install python@3.11 git
40
+ ```
41
+ For Termux:
42
+ ```
43
+ pkg install python -y
44
+ pkg install git -y
45
+ ```
46
+
47
+ <a name="i-2"></a>
48
+
49
+ **2.Download repository:**
50
+ ```
51
+ git clone https://github.com/TheCaduceus/FileStreamBot.git
52
+ ```
53
+
54
+ **3.Change Directory:**
55
+
56
+ ```
57
+ cd FileStreamBot
58
+ ```
59
+
60
+ <a name="i-3"></a>
61
+
62
+ **4.Install requirements:**
63
+
64
+ ```
65
+ pip install -r requirements.txt
66
+ ```
67
+
68
+ <a name="variables"></a>
69
+
70
+ ## 📝 Variables
71
+ **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.**
72
+ * `API_ID`|`TELEGRAM_API_ID`: API ID of your Telegram account, can be obtained from [My Telegram](https://my.telegram.org). `int`
73
+ * `API_HASH`|`TELEGRAM_API_HASH`: API hash of your Telegram account, can be obtained from [My Telegram](https://my.telegram.org). `str`
74
+ * `OWNER_ID`: ID of your Telegram account, can be obtained by sending **/info** to [@DrFileStreamBot](https://t.me/DrFileStreamBot). `int`
75
+ * `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`
76
+ * `BOT_USERNAME`|`TELEGRAM_BOT_USERNAME`: Username of your Telegram bot, create one using [@BotFather](https://t.me/BotFather). `str`
77
+ * `BOT_TOKEN`|`TELEGRAM_BOT_TOKEN`: Telegram API token of your bot, can be obtained from [@BotFather](https://t.me/BotFather). `str`
78
+ * `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`
79
+ * `BOT_WORKERS`: Number of updates bot should process from Telegram at once, by default to 10 updates. `int`
80
+ * `SECRET_CODE_LENGTH`: Number of characters that file code should contain, by default to 12 characters. `int`
81
+ * `BASE_URL`: Base URL that bot should use while generating file links, can be FQDN and by default to `127.0.0.1`. `str`
82
+ * `BIND_ADDRESS`: Bind address for web server, by default to `0.0.0.0` to run on all possible addresses. `str`
83
+ * `PORT`: Port for web server to run on, by default to `8080`. `int`
84
+
85
+ ## 🕹 Deployment
86
+
87
+ <a name="d-1"></a>
88
+
89
+ **1.Running locally:**
90
+ ```
91
+ python -m bot
92
+ ```
93
+
94
+ <a name="d-2"></a>
95
+
96
+ **2.Using Docker:** *(Recommended)*
97
+ * Build own Docker image:
98
+ ```
99
+ docker build -t file-stream-bot .
100
+ ```
101
+ * Run the Docker container:
102
+ ```
103
+ docker run -p 8080:8080 file-stream-bot
104
+ ```
105
+
106
+ <a name="help"></a>
107
+
108
+ ## ⛑️ Need help!
109
+ - Ask questions or doubts [here](https://t.me/DrDiscussion).
110
+
111
+ <a name="credits"></a>
112
+
113
+ ## ❤️ Credits & Thanks
114
+
115
+ [**Dr.Caduceus**](https://github.com/TheCaduceus): Owner & developer of Microsoft E5 Auto Renewal Tool.<br>
bot/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram import Client
2
+ from logging import getLogger
3
+ from logging.config import dictConfig
4
+ from .config import Telegram, LOGGER_CONFIG_JSON
5
+
6
+ dictConfig(LOGGER_CONFIG_JSON)
7
+
8
+ version = 1.0
9
+ logger = getLogger('bot')
10
+
11
+ TelegramBot = Client(
12
+ name="bot",
13
+ api_id = Telegram.API_ID,
14
+ api_hash = Telegram.API_HASH,
15
+ bot_token = Telegram.BOT_TOKEN,
16
+ plugins={"root": "bot/plugins"},
17
+ workers = Telegram.BOT_WORKERS,
18
+ max_concurrent_transmissions=1000
19
+ )
bot/__main__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ from bot import TelegramBot, logger
2
+ from bot.server import server
3
+
4
+ if __name__ == '__main__':
5
+ logger.info('Initializing...')
6
+ TelegramBot.loop.create_task(server.serve())
7
+ TelegramBot.run()
bot/config.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from os import environ as env
2
+
3
+ class Telegram:
4
+ API_ID = env.get("TELEGRAM_API_ID", 1234)
5
+ API_HASH = env.get("TELEGRAM_API_HASH", "xyz")
6
+ OWNER_ID = int(env.get("OWNER_ID", 1234567890))
7
+ ALLOWED_USER_IDS = env.get("ALLOWED_USER_IDS", "").split()
8
+ BOT_USERNAME = env.get("TELEGRAM_BOT_USERNAME", "BotFather")
9
+ BOT_TOKEN = env.get("TELEGRAM_BOT_TOKEN", "1234:abcd")
10
+ CHANNEL_ID = int(env.get("TELEGRAM_CHANNEL_ID", -1001234567890))
11
+ BOT_WORKERS = int(env.get("BOT_WORKERS", 10))
12
+ SECRET_CODE_LENGTH = int(env.get("SECRET_CODE_LENGTH", 12))
13
+
14
+ class Server:
15
+ BASE_URL = env.get("BASE_URL", "http://127.0.0.1:8080")
16
+ BIND_ADDRESS = env.get("BIND_ADDRESS", "0.0.0.0")
17
+ PORT = int(env.get("PORT", 8080))
18
+
19
+ # LOGGING CONFIGURATION
20
+ LOGGER_CONFIG_JSON = {
21
+ 'version': 1,
22
+ 'formatters': {
23
+ 'default': {
24
+ 'format': '[%(asctime)s][%(name)s][%(levelname)s] -> %(message)s',
25
+ 'datefmt': '%d/%m/%Y %H:%M:%S'
26
+ },
27
+ },
28
+ 'handlers': {
29
+ 'file_handler': {
30
+ 'class': 'logging.FileHandler',
31
+ 'filename': 'event-log.txt',
32
+ 'formatter': 'default'
33
+ },
34
+ 'stream_handler': {
35
+ 'class': 'logging.StreamHandler',
36
+ 'formatter': 'default'
37
+ }
38
+ },
39
+ 'loggers': {
40
+ 'uvicorn': {
41
+ 'level': 'INFO',
42
+ 'handlers': ['file_handler', 'stream_handler']
43
+ },
44
+ 'uvicorn.error': {
45
+ 'level': 'WARNING',
46
+ 'handlers': ['file_handler', 'stream_handler']
47
+ },
48
+ 'bot': {
49
+ 'level': 'INFO',
50
+ 'handlers': ['file_handler', 'stream_handler']
51
+ }
52
+ }
53
+ }
bot/modules/decorators.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram import Client
2
+ from pyrogram.types import Message, CallbackQuery
3
+ from typing import Union, Callable
4
+ from functools import wraps
5
+ from bot.config import Telegram
6
+
7
+ def verify_user(func: Callable):
8
+
9
+ @wraps(func)
10
+ async def decorator(client: Client, update: Union[Message, CallbackQuery]):
11
+ user_id = str(update.from_user.id)
12
+
13
+ if not Telegram.ALLOWED_USER_IDS or user_id in Telegram.ALLOWED_USER_IDS:
14
+ return await func(client, update)
15
+
16
+ return decorator
bot/modules/static.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ WelcomeText = \
2
+ """
3
+ Hi **%(first_name)s**, send me a file or add me as an admin to any channel to instantly generate file links.
4
+
5
+ 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.
6
+
7
+ - /start to get this message.
8
+ - /info to get user info.
9
+ - /log to get bot logs. (admin only!)
10
+ """
11
+
12
+ FileLinksText = \
13
+ """
14
+ **Download Link:**
15
+ `%(dl_link)s`
16
+ **Telegram File:**
17
+ `%(tg_link)s`
18
+ """
19
+
20
+ MediaLinksText = \
21
+ """
22
+ **Download Link:**
23
+ `%(dl_link)s`
24
+ **Stream Link:**
25
+ `%(stream_link)s`
26
+ **Telegram File:**
27
+ `%(tg_link)s`
28
+ """
29
+
30
+ InvalidQueryText = \
31
+ """
32
+ Query data mismatched.
33
+ """
34
+
35
+ MessageNotExist = \
36
+ """
37
+ File revoked or not exist.
38
+ """
39
+
40
+ LinkRevokedText = \
41
+ """
42
+ The link has been revoked. It may take some time for the changes to take effect.
43
+ """
44
+
45
+ InvalidPayloadText = \
46
+ """
47
+ Invalid payload.
48
+ """
bot/modules/telegram.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram.types import Message
2
+ from datetime import datetime
3
+ from mimetypes import guess_type
4
+ from bot import TelegramBot
5
+ from bot.config import Telegram
6
+ from bot.server.error import abort
7
+
8
+
9
+ async def get_message(message_id: int):
10
+ message = None
11
+
12
+ try:
13
+ message = await TelegramBot.get_messages(
14
+ chat_id=Telegram.CHANNEL_ID,
15
+ message_ids=message_id
16
+ )
17
+ if message.empty: message = None
18
+ except Exception:
19
+ pass
20
+
21
+ return message
22
+
23
+ async def get_file_properties(msg: Message):
24
+ attributes = (
25
+ 'document',
26
+ 'video',
27
+ 'audio',
28
+ 'voice',
29
+ 'photo',
30
+ 'video_note'
31
+ )
32
+
33
+ for attribute in attributes:
34
+ media = getattr(msg, attribute, None)
35
+ if media:
36
+ file_type = attribute
37
+ break
38
+
39
+ if not media: abort(400, 'Unknown file type.')
40
+
41
+ file_name = getattr(media, 'file_name', None)
42
+
43
+ if not file_name:
44
+ file_format = {
45
+ 'video': 'mp4',
46
+ 'audio': 'mp3',
47
+ 'voice': 'ogg',
48
+ 'photo': 'jpg',
49
+ 'video_note': 'mp4'
50
+ }.get(attribute)
51
+ date = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
52
+ file_name = f'{file_type}-{date}.{file_format}'
53
+
54
+ mime_type = guess_type(file_name)[0] or 'application/octet-stream'
55
+
56
+ return file_name, mime_type
bot/plugins/callback.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram.types import CallbackQuery
2
+ from bot import TelegramBot
3
+ from bot.modules.static import *
4
+ from bot.modules.decorators import verify_user
5
+ from bot.modules.telegram import get_message
6
+
7
+ @TelegramBot.on_callback_query()
8
+ @verify_user
9
+ async def manage_callback(bot, q: CallbackQuery):
10
+ query = q.data
11
+ if query.startswith('rm_'):
12
+ sq = query.split('_')
13
+
14
+ if len(sq) != 3:
15
+ return await q.answer(InvalidQueryText, show_alert=True)
16
+
17
+ message = await get_message(int(sq[1]))
18
+
19
+ if not message:
20
+ return await q.answer(MessageNotExist, show_alert=True)
21
+ if sq[2] != message.caption:
22
+ return await q.answer(InvalidQueryText, show_alert=True)
23
+
24
+ await message.delete()
25
+ await q.answer(LinkRevokedText, show_alert=True)
26
+ else:
27
+ await q.answer(InvalidQueryText, show_alert=True)
bot/plugins/commands.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram import filters
2
+ from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
3
+ from aiofiles import open as async_open
4
+ from aiofiles.os import remove as async_rm
5
+ from bot import TelegramBot, logger
6
+ from bot.config import Telegram
7
+ from bot.modules.static import *
8
+ from .deeplinks import deeplinks
9
+ from bot.modules.decorators import verify_user
10
+
11
+ @TelegramBot.on_message(filters.command('start') & filters.private)
12
+ @verify_user
13
+ async def start(_, msg: Message):
14
+ if len(msg.command) != 1:
15
+ return await deeplinks(msg, msg.command[1])
16
+
17
+ await msg.reply(
18
+ text=WelcomeText % {'first_name': msg.from_user.first_name},
19
+ quote=True,
20
+ reply_markup=InlineKeyboardMarkup(
21
+ [
22
+ [
23
+ InlineKeyboardButton('Add to Channel', url=f'https://t.me/{Telegram.BOT_USERNAME}?startchannel&admin=post_messages+edit_messages+delete_messages')
24
+ ]
25
+ ]
26
+ )
27
+ )
28
+
29
+ @TelegramBot.on_message(filters.command('info') & filters.private)
30
+ @verify_user
31
+ async def user_info(_, msg: Message):
32
+ await msg.reply(text=f'`{msg.from_user}`', quote=True)
33
+
34
+ filename = f'{msg.from_user.id}.json'
35
+ async with async_open(filename, "w") as file:
36
+ await file.write(f'{msg.from_user}')
37
+
38
+ await msg.reply_document(filename)
39
+ await async_rm(filename)
40
+
41
+ @TelegramBot.on_message(filters.private & filters.command('log') & filters.user(Telegram.OWNER_ID))
42
+ async def send_log(_, msg: Message):
43
+ await msg.reply_document('event-log.txt', quote=True)
44
+
45
+ logger.info('Bot is now started!')
bot/plugins/deeplinks.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram.types import Message
2
+ from bot.modules.static import *
3
+ from bot.modules.telegram import get_message
4
+
5
+ async def deeplinks(msg: Message, payload: str):
6
+ if payload.startswith('file_'):
7
+ sp = payload.split('_')
8
+
9
+ if len(sp) != 3:
10
+ return await msg.reply(InvalidPayloadText, quote=True)
11
+
12
+ message = await get_message(int(sp[1]))
13
+
14
+ if not message:
15
+ return await msg.reply(MessageNotExist)
16
+ if sp[2] != message.caption:
17
+ return await msg.reply(InvalidPayloadText, quote=True)
18
+
19
+ await message.copy(chat_id=msg.from_user.id, caption="")
20
+ else:
21
+ await msg.reply(InvalidPayloadText, quote=True)
bot/plugins/files.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pyrogram import filters, errors
2
+ from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
3
+ from secrets import token_hex
4
+ from bot import TelegramBot
5
+ from bot.config import Telegram, Server
6
+ from bot.modules.decorators import verify_user
7
+ from bot.modules.static import *
8
+
9
+ @TelegramBot.on_message(
10
+ filters.private
11
+ & (
12
+ filters.document
13
+ | filters.video
14
+ | filters.video_note
15
+ | filters.audio
16
+ | filters.voice
17
+ | filters.photo
18
+ )
19
+ )
20
+ @verify_user
21
+ async def handle_user_file(_, msg: Message):
22
+ secret_code = token_hex(Telegram.SECRET_CODE_LENGTH)
23
+ file = await msg.copy(
24
+ chat_id=Telegram.CHANNEL_ID,
25
+ caption=f'`{secret_code}`'
26
+ )
27
+ file_id = file.id
28
+
29
+ dl_link = f'{Server.BASE_URL}/dl/{file_id}?code={secret_code}'
30
+ tg_link = f'{Server.BASE_URL}/file/{file_id}?code={secret_code}'
31
+ deep_link = f'https://t.me/{Telegram.BOT_USERNAME}?start=file_{file_id}_{secret_code}'
32
+
33
+ if (msg.document and 'video' in msg.document.mime_type) or msg.video:
34
+ stream_link = f'{Server.BASE_URL}/stream/{file_id}?code={secret_code}'
35
+ await msg.reply(
36
+ text=MediaLinksText % {'dl_link': dl_link, 'tg_link': tg_link, 'stream_link': stream_link},
37
+ quote=True,
38
+ reply_markup=InlineKeyboardMarkup(
39
+ [
40
+ [
41
+ InlineKeyboardButton('Download', url=dl_link),
42
+ InlineKeyboardButton('Stream', url=stream_link)
43
+ ],
44
+ [
45
+ InlineKeyboardButton('Get File', url=deep_link),
46
+ InlineKeyboardButton('Revoke', callback_data=f'rm_{file_id}_{secret_code}')
47
+ ]
48
+ ]
49
+ )
50
+ )
51
+ else:
52
+ await msg.reply(
53
+ text=FileLinksText % {'dl_link': dl_link, 'tg_link': tg_link},
54
+ quote=True,
55
+ reply_markup=InlineKeyboardMarkup(
56
+ [
57
+ [
58
+ InlineKeyboardButton('Download', url=dl_link),
59
+ InlineKeyboardButton('Get File', url=deep_link)
60
+ ],
61
+ [
62
+ InlineKeyboardButton('Revoke', callback_data=f'rm_{file_id}_{secret_code}')
63
+ ]
64
+ ]
65
+ )
66
+ )
67
+
68
+ @TelegramBot.on_message(
69
+ filters.channel
70
+ & ~filters.forwarded
71
+ & ~filters.media_group
72
+ & (
73
+ filters.document
74
+ | filters.video
75
+ | filters.video_note
76
+ | filters.audio
77
+ | filters.voice
78
+ | filters.photo
79
+ )
80
+ )
81
+ @verify_user
82
+ async def handle_channel_file(_, msg: Message):
83
+ if msg.caption and '#pass' in msg.caption:
84
+ return
85
+
86
+ secret_code = token_hex(Telegram.SECRET_CODE_LENGTH)
87
+
88
+ try:
89
+ file = await msg.copy(
90
+ chat_id=Telegram.CHANNEL_ID,
91
+ caption=f'`{secret_code}`'
92
+ )
93
+ except (errors.ChatForwardsRestricted, errors.MessageIdInvalid, errors.ChannelPrivate):
94
+ return
95
+
96
+ file_id = file.id
97
+
98
+ dl_link = f'{Server.BASE_URL}/dl/{file_id}?code={secret_code}'
99
+ tg_link = f'{Server.BASE_URL}/file/{file_id}?code={secret_code}'
100
+
101
+ if (msg.document and 'video' in msg.document.mime_type) or msg.video:
102
+ stream_link = f'{Server.BASE_URL}/stream/{file_id}?code={secret_code}'
103
+ await msg.edit_reply_markup(
104
+ InlineKeyboardMarkup(
105
+ [
106
+ [
107
+ InlineKeyboardButton('Download', url=dl_link),
108
+ InlineKeyboardButton('Stream', url=stream_link)
109
+ ],
110
+ [
111
+ InlineKeyboardButton('Get File', url=tg_link)
112
+ ]
113
+ ]
114
+ )
115
+ )
116
+ else:
117
+ await msg.edit_reply_markup(
118
+ InlineKeyboardMarkup(
119
+ [
120
+ [
121
+ InlineKeyboardButton('Download', url=dl_link),
122
+ InlineKeyboardButton('Get File', url=tg_link)
123
+ ]
124
+ ]
125
+ )
126
+ )
bot/server/__init__.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from quart import Quart
2
+ from uvicorn import Config, Server as UvicornServer
3
+ from logging import getLogger
4
+ from bot.config import Server, LOGGER_CONFIG_JSON
5
+
6
+ from . import main, error
7
+
8
+ logger = getLogger('uvicorn')
9
+ instance = Quart(__name__)
10
+
11
+ @instance.before_serving
12
+ async def before_serve():
13
+ logger.info('Web server is started!')
14
+ logger.info(f'Server running on {Server.BIND_ADDRESS}:{Server.PORT}')
15
+
16
+ instance.register_blueprint(main.bp)
17
+
18
+ instance.register_error_handler(400, error.invalid_request)
19
+ instance.register_error_handler(404, error.not_found)
20
+ instance.register_error_handler(405, error.invalid_method)
21
+ instance.register_error_handler(error.HTTPError, error.http_error)
22
+
23
+ server = UvicornServer(
24
+ Config(
25
+ app = instance,
26
+ host = Server.BIND_ADDRESS,
27
+ port = Server.PORT,
28
+ log_config = LOGGER_CONFIG_JSON
29
+ )
30
+ )
bot/server/error.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class HTTPError(Exception):
2
+ status_code:int = None
3
+ description:str = None
4
+ def __init__(self, status_code, description):
5
+ self.status_code = status_code
6
+ self.description = description
7
+ super().__init__(self.status_code, self.description)
8
+
9
+ error_messages = {
10
+ 400: 'Invalid request.',
11
+ 401: 'File code is required to download the file.',
12
+ 403: 'Invalid file code.',
13
+ 404: 'File not found.',
14
+ 500: 'Internal server error.'
15
+ }
16
+
17
+ async def invalid_request(_):
18
+ return 'Invalid request.', 400
19
+
20
+ async def not_found(_):
21
+ return 'Resource not found.', 404
22
+
23
+ async def invalid_method(_):
24
+ return 'Invalid request method.', 405
25
+
26
+ async def http_error(error: HTTPError):
27
+ error_message = error_messages.get(error.status_code)
28
+ return error.description or error_message, error.status_code
29
+
30
+ def abort(status_code: int = 500, description: str = None):
31
+ raise HTTPError(status_code, description)
bot/server/main.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from quart import Blueprint, Response, request, render_template, redirect
2
+ from .error import abort
3
+ from bot import version, TelegramBot
4
+ from bot.config import Telegram, Server
5
+ from bot.modules.telegram import get_message, get_file_properties
6
+
7
+ bp = Blueprint('main', __name__)
8
+
9
+ @bp.route('/')
10
+ async def home():
11
+ return redirect(f'https://t.me/{Telegram.BOT_USERNAME}')
12
+
13
+ @bp.route('/dl/<int:file_id>')
14
+ async def transmit_file(file_id):
15
+ file = await get_message(int(file_id)) or abort(404)
16
+ code = request.args.get('code') or abort(401)
17
+
18
+ if code != file.caption:
19
+ abort(403)
20
+
21
+ file_name, mime_type = await get_file_properties(file)
22
+ headers = {
23
+ 'Content-Type': mime_type,
24
+ 'Content-Disposition': f'attachment; filename="{file_name}"'
25
+ }
26
+
27
+ file_stream = TelegramBot.stream_media(file)
28
+
29
+ return Response(file_stream, headers=headers)
30
+
31
+ @bp.route('/stream/<int:file_id>')
32
+ async def stream_file(file_id):
33
+ code = request.args.get('code') or abort(401)
34
+
35
+ return await render_template('player.html', mediaLink=f'{Server.BASE_URL}/dl/{file_id}?code={code}')
36
+
37
+ @bp.route('/file/<int:file_id>')
38
+ async def file_deeplink(file_id):
39
+ code = request.args.get('code') or abort(401)
40
+
41
+ return redirect(f'https://t.me/{Telegram.BOT_USERNAME}?start=file_{file_id}_{code}')
bot/server/templates/player.html ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <title>Play Files</title>
6
+
7
+ <meta charset="UTF-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
9
+ <meta http-equiv="X-Frame-Options" content="deny">
10
+
11
+ <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
13
+
14
+ <script src="https://cdn.plyr.io/3.7.8/plyr.polyfilled.js"></script>
15
+
16
+ <style>
17
+ html, body {
18
+ margin: 0;
19
+ height: 100%;
20
+ }
21
+
22
+ #stream-media {
23
+ height: 100%;
24
+ width: 100%;
25
+ }
26
+
27
+ #error-message {
28
+ color: red;
29
+ font-size: 24px;
30
+ text-align: center;
31
+ margin-top: 20px;
32
+ }
33
+
34
+ .plyr__video-wrapper .plyr-download-button{
35
+ position: absolute;
36
+ top: 10px;
37
+ left: 10px;
38
+ width: 30px;
39
+ height: 30px;
40
+ background-color: rgba(0, 0, 0, 0.7);
41
+ border-radius: 50%;
42
+ text-align: center;
43
+ line-height: 30px;
44
+ color: white;
45
+ z-index: 10;
46
+ }
47
+
48
+ .plyr__volume {
49
+ max-width: initial;
50
+ min-width: initial;
51
+ width: auto;
52
+ position: relative;
53
+ }
54
+
55
+
56
+ .plyr__video-wrapper .plyr-share-button{
57
+ position: absolute;
58
+ top: 50px;
59
+ left: 10px;
60
+ width: 30px;
61
+ height: 30px;
62
+ background-color: rgba(0, 0, 0, 0.7);
63
+ border-radius: 50%;
64
+ text-align: center;
65
+ line-height: 30px;
66
+ color: white;
67
+ z-index: 10;
68
+ }
69
+
70
+ .plyr__video-wrapper .plyr-download-button:hover,
71
+ .plyr__video-wrapper .plyr-share-button:hover{
72
+ background-color: rgba(255, 255, 255, 0.7);
73
+ color: black;
74
+ }
75
+
76
+ .plyr__video-wrapper .plyr-download-button:before {
77
+ font-family: "Font Awesome 5 Free";
78
+ content: "\f019";
79
+ font-weight: bold;
80
+ }
81
+
82
+ .plyr__video-wrapper .plyr-share-button:before {
83
+ font-family: "Font Awesome 5 Free";
84
+ content: "\f064";
85
+ font-weight: bold;
86
+ }
87
+
88
+ .plyr, .plyr__video-wrapper, .plyr__video-embed iframe {
89
+ height: 100%;
90
+ }
91
+
92
+ </style>
93
+ </head>
94
+
95
+ <body>
96
+ <video id="stream-media" controls preload="auto">
97
+ <source src="" type="">
98
+ <p class="vjs-no-js">
99
+ To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video
100
+ </p>
101
+ </video>
102
+
103
+ <div id="error-message"></div>
104
+
105
+ <script>
106
+ var player = new Plyr('#stream-media', {
107
+ controls:['play-large', 'rewind', 'play', 'fast-forward', 'progress', 'current-time', 'mute', 'settings', 'pip', 'fullscreen'],
108
+ settings:['speed','loop'],
109
+ speed:{selected:1,options:[0.25,0.5,0.75,1,1.25,1.5,1.75,2]},
110
+ seek: 10,
111
+ keyboard: { focused: true, global: true },
112
+ });
113
+
114
+ var mediaLink = "{{ mediaLink }}";
115
+
116
+ if (mediaLink) {
117
+ document.querySelector('#stream-media source').setAttribute('src', mediaLink);
118
+ player.restart();
119
+
120
+ var downloadButton = document.createElement('div');
121
+ downloadButton.className = 'plyr-download-button';
122
+
123
+ downloadButton.onclick = function() {
124
+ event.stopPropagation();
125
+ var link = document.createElement('a');
126
+ link.href = mediaLink;
127
+ document.body.appendChild(link);
128
+ link.click();
129
+ document.body.removeChild(link);
130
+ };
131
+
132
+ player.elements.container.querySelector('.plyr__video-wrapper').appendChild(downloadButton);
133
+
134
+ var shareButton = document.createElement('div');
135
+ shareButton.className = 'plyr-share-button';
136
+
137
+ shareButton.onclick = function() {
138
+ event.stopPropagation();
139
+ if (navigator.share) {
140
+ navigator.share({
141
+ title: "Play",
142
+ url: window.location.href
143
+ });
144
+ }
145
+ };
146
+
147
+ player.elements.container.querySelector('.plyr__video-wrapper').appendChild(shareButton);
148
+
149
+ } else {
150
+ document.getElementById('error-message').textContent = 'Error: Media URL not provided';
151
+ }
152
+ </script>
153
+
154
+ </body>
155
+ </html>
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ pyrogram
2
+ tgcrypto
3
+ quart
4
+ uvicorn
5
+ aiofiles
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.11.6