Spaces:
Sleeping
Sleeping
Dr.Caduceus
commited on
Bump to v1.7 (Migrate to Hydrogram)
Browse files- README.md +4 -6
- bot/__init__.py +10 -6
- bot/__main__.py +2 -18
- bot/config.py +10 -5
- bot/modules/decorators.py +10 -14
- bot/modules/static.py +3 -28
- bot/modules/telegram.py +27 -36
- bot/plugins/callback.py +21 -14
- bot/plugins/commands.py +12 -22
- bot/plugins/deeplinks.py +22 -21
- bot/plugins/files.py +45 -80
- bot/server/__init__.py +1 -1
- bot/server/main.py +52 -54
- requirements.txt +2 -2
README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
| 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**
|
|
@@ -34,7 +32,7 @@ For Linux:
|
|
| 34 |
```
|
| 35 |
sudo apt-get update && sudo apt-get install -y python3.11 git pip
|
| 36 |
```
|
| 37 |
-
For
|
| 38 |
```
|
| 39 |
brew install python@3.11 git
|
| 40 |
```
|
|
@@ -71,13 +69,13 @@ pip install -r requirements.txt
|
|
| 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 [@
|
| 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 [@
|
| 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
|
| 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`
|
|
|
|
| 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 |
</div><br>
|
| 4 |
|
| 5 |
## **📑 INDEX**
|
|
|
|
| 32 |
```
|
| 33 |
sudo apt-get update && sudo apt-get install -y python3.11 git pip
|
| 34 |
```
|
| 35 |
+
For MacOS:
|
| 36 |
```
|
| 37 |
brew install python@3.11 git
|
| 38 |
```
|
|
|
|
| 69 |
**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.**
|
| 70 |
* `API_ID`|`TELEGRAM_API_ID`: API ID of your Telegram account, can be obtained from [My Telegram](https://my.telegram.org). `int`
|
| 71 |
* `API_HASH`|`TELEGRAM_API_HASH`: API hash of your Telegram account, can be obtained from [My Telegram](https://my.telegram.org). `str`
|
| 72 |
+
* `OWNER_ID`: ID of your Telegram account, can be obtained by sending **/info** to [@DumpJsonBot](https://t.me/DumpJsonBot). `int`
|
| 73 |
* `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`
|
| 74 |
* `BOT_USERNAME`|`TELEGRAM_BOT_USERNAME`: Username of your Telegram bot, create one using [@BotFather](https://t.me/BotFather). `str`
|
| 75 |
* `BOT_TOKEN`|`TELEGRAM_BOT_TOKEN`: Telegram API token of your bot, can be obtained from [@BotFather](https://t.me/BotFather). `str`
|
| 76 |
+
* `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`
|
| 77 |
* `BOT_WORKERS`: Number of updates bot should process from Telegram at once, by default to 10 updates. `int`
|
| 78 |
+
* `SECRET_CODE_LENGTH`: Number of characters that file code should contain, by default to 24 characters. `int`
|
| 79 |
* `BASE_URL`: Base URL that bot should use while generating file links, can be FQDN and by default to `127.0.0.1`. `str`
|
| 80 |
* `BIND_ADDRESS`: Bind address for web server, by default to `0.0.0.0` to run on all possible addresses. `str`
|
| 81 |
* `PORT`: Port for web server to run on, by default to `8080`. `int`
|
bot/__init__.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
| 1 |
-
from
|
| 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.
|
| 9 |
logger = getLogger('bot')
|
| 10 |
|
| 11 |
-
TelegramBot =
|
| 12 |
-
|
| 13 |
-
api_id=Telegram.API_ID,
|
| 14 |
-
api_hash=Telegram.API_HASH
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
)
|
|
|
|
| 1 |
+
from hydrogram 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.7
|
| 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
CHANGED
|
@@ -1,22 +1,6 @@
|
|
| 1 |
-
from
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
from bot import TelegramBot, logger
|
| 4 |
-
from bot.config import Telegram
|
| 5 |
from bot.server import server
|
| 6 |
|
| 7 |
-
def load_plugins():
|
| 8 |
-
count = 0
|
| 9 |
-
for path in Path('bot/plugins').rglob('*.py'):
|
| 10 |
-
import_module(f'bot.plugins.{path.stem}')
|
| 11 |
-
count += 1
|
| 12 |
-
logger.info(f'Loaded {count} {"plugins" if count > 1 else "plugin"}.')
|
| 13 |
-
|
| 14 |
if __name__ == '__main__':
|
| 15 |
-
logger.info('initializing...')
|
| 16 |
TelegramBot.loop.create_task(server.serve())
|
| 17 |
-
TelegramBot.
|
| 18 |
-
logger.info('Telegram client is now started.')
|
| 19 |
-
logger.info('Loading bot plugins...')
|
| 20 |
-
load_plugins()
|
| 21 |
-
logger.info('Bot is now ready!')
|
| 22 |
-
TelegramBot.run_until_disconnected()
|
|
|
|
| 1 |
+
from bot import TelegramBot
|
|
|
|
|
|
|
|
|
|
| 2 |
from bot.server import server
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
if __name__ == '__main__':
|
|
|
|
| 5 |
TelegramBot.loop.create_task(server.serve())
|
| 6 |
+
TelegramBot.run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bot/config.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
| 1 |
from os import environ as env
|
| 2 |
|
| 3 |
class Telegram:
|
| 4 |
-
API_ID = int(env.get("TELEGRAM_API_ID",
|
| 5 |
API_HASH = env.get("TELEGRAM_API_HASH", "xyz")
|
| 6 |
-
OWNER_ID = int(env.get("OWNER_ID",
|
| 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", "
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
class Server:
|
| 14 |
BASE_URL = env.get("BASE_URL", "http://127.0.0.1:8080")
|
|
@@ -47,6 +48,10 @@ LOGGER_CONFIG_JSON = {
|
|
| 47 |
'bot': {
|
| 48 |
'level': 'INFO',
|
| 49 |
'handlers': ['file_handler', 'stream_handler']
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
}
|
| 52 |
}
|
|
|
|
| 1 |
from os import environ as env
|
| 2 |
|
| 3 |
class Telegram:
|
| 4 |
+
API_ID = int(env.get("TELEGRAM_API_ID", 12345))
|
| 5 |
API_HASH = env.get("TELEGRAM_API_HASH", "xyz")
|
| 6 |
+
OWNER_ID = int(env.get("OWNER_ID", 5530237028))
|
| 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", "1234567:xyz")
|
| 10 |
+
BOT_WORKERS = env.get("BOT_WORKERS", 10)
|
| 11 |
+
CHANNEL_ID = int(env.get("TELEGRAM_CHANNEL_ID", -100123456789))
|
| 12 |
+
SECRET_CODE_LENGTH = int(env.get("SECRET_CODE_LENGTH", 24))
|
| 13 |
|
| 14 |
class Server:
|
| 15 |
BASE_URL = env.get("BASE_URL", "http://127.0.0.1:8080")
|
|
|
|
| 48 |
'bot': {
|
| 49 |
'level': 'INFO',
|
| 50 |
'handlers': ['file_handler', 'stream_handler']
|
| 51 |
+
},
|
| 52 |
+
'hydrogram': {
|
| 53 |
+
'level': 'INFO',
|
| 54 |
+
'handlers': ['file_handler', 'stream_handler']
|
| 55 |
}
|
| 56 |
}
|
| 57 |
}
|
bot/modules/decorators.py
CHANGED
|
@@ -1,20 +1,16 @@
|
|
| 1 |
-
from
|
| 2 |
-
from
|
|
|
|
| 3 |
from functools import wraps
|
| 4 |
from bot.config import Telegram
|
| 5 |
|
| 6 |
-
def verify_user(
|
| 7 |
-
|
| 8 |
-
def decorator(func: Callable):
|
| 9 |
-
@wraps(func)
|
| 10 |
-
async def wrapper(update: NewMessage.Event | CallbackQuery.Event):
|
| 11 |
-
if private and not update.is_private:
|
| 12 |
-
return
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
return wrapper
|
| 20 |
return decorator
|
|
|
|
| 1 |
+
from hydrogram import Client
|
| 2 |
+
from hydrogram.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 |
+
chat_id = str(update.from_user.id if update.from_user else update.chat.id)
|
| 12 |
|
| 13 |
+
if not Telegram.ALLOWED_USER_IDS or chat_id in Telegram.ALLOWED_USER_IDS:
|
| 14 |
+
return await func(client, update)
|
| 15 |
+
|
|
|
|
| 16 |
return decorator
|
bot/modules/static.py
CHANGED
|
@@ -1,35 +1,17 @@
|
|
| 1 |
WelcomeText = \
|
| 2 |
"""
|
| 3 |
-
Hi **%(first_name)s**, send me a file
|
| 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 |
-
|
| 13 |
"""
|
| 14 |
-
|
| 15 |
-
`{sender.first_name}`
|
| 16 |
-
|
| 17 |
-
**Last Name:**
|
| 18 |
-
`{sender.last_name}`
|
| 19 |
-
|
| 20 |
-
**User ID:**
|
| 21 |
-
`{sender.id}`
|
| 22 |
-
|
| 23 |
-
**Username:**
|
| 24 |
-
`@{sender.username}`
|
| 25 |
"""
|
| 26 |
|
| 27 |
FileLinksText = \
|
| 28 |
"""
|
| 29 |
**Download Link:**
|
| 30 |
`%(dl_link)s`
|
| 31 |
-
**Telegram File:**
|
| 32 |
-
`%(tg_link)s`
|
| 33 |
"""
|
| 34 |
|
| 35 |
MediaLinksText = \
|
|
@@ -38,8 +20,6 @@ MediaLinksText = \
|
|
| 38 |
`%(dl_link)s`
|
| 39 |
**Stream Link:**
|
| 40 |
`%(stream_link)s`
|
| 41 |
-
**Telegram File:**
|
| 42 |
-
`%(tg_link)s`
|
| 43 |
"""
|
| 44 |
|
| 45 |
InvalidQueryText = \
|
|
@@ -61,8 +41,3 @@ InvalidPayloadText = \
|
|
| 61 |
"""
|
| 62 |
Invalid payload.
|
| 63 |
"""
|
| 64 |
-
|
| 65 |
-
MediaTypeNotSupportedText = \
|
| 66 |
-
"""
|
| 67 |
-
Sorry, this media type is not supported.
|
| 68 |
-
"""
|
|
|
|
| 1 |
WelcomeText = \
|
| 2 |
"""
|
| 3 |
+
Hi **%(first_name)s**, send me a file to instantly generate file links.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
+
PrivacyText = \
|
| 7 |
"""
|
| 8 |
+
This bot securely stores your files to deliver its service.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
FileLinksText = \
|
| 12 |
"""
|
| 13 |
**Download Link:**
|
| 14 |
`%(dl_link)s`
|
|
|
|
|
|
|
| 15 |
"""
|
| 16 |
|
| 17 |
MediaLinksText = \
|
|
|
|
| 20 |
`%(dl_link)s`
|
| 21 |
**Stream Link:**
|
| 22 |
`%(stream_link)s`
|
|
|
|
|
|
|
| 23 |
"""
|
| 24 |
|
| 25 |
InvalidQueryText = \
|
|
|
|
| 41 |
"""
|
| 42 |
Invalid payload.
|
| 43 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bot/modules/telegram.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
-
from
|
| 2 |
-
from telethon.tl.custom import Message
|
| 3 |
from datetime import datetime
|
| 4 |
from mimetypes import guess_type
|
| 5 |
from bot import TelegramBot
|
|
@@ -10,55 +9,47 @@ async def get_message(message_id: int) -> Message | None:
|
|
| 10 |
message = None
|
| 11 |
|
| 12 |
try:
|
| 13 |
-
message = await TelegramBot.get_messages(Telegram.CHANNEL_ID,
|
|
|
|
| 14 |
except Exception:
|
| 15 |
pass
|
| 16 |
|
| 17 |
return message
|
| 18 |
|
| 19 |
-
async def send_message(
|
| 20 |
-
return await TelegramBot.send_message(entity=send_to, message=
|
| 21 |
-
|
| 22 |
-
def
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
or update.gif
|
| 31 |
-
)
|
| 32 |
-
and not update.sticker
|
| 33 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
file_size = message.file.size or 0
|
| 38 |
-
mime_type = message.file.mime_type
|
| 39 |
|
| 40 |
if not file_name:
|
| 41 |
-
|
| 42 |
'video': 'mp4',
|
| 43 |
'audio': 'mp3',
|
| 44 |
'voice': 'ogg',
|
| 45 |
'photo': 'jpg',
|
| 46 |
'video_note': 'mp4'
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
for attribute in attributes:
|
| 50 |
-
media = getattr(message, attribute, None)
|
| 51 |
-
if media:
|
| 52 |
-
file_type, file_format = attribute, attributes[attribute]
|
| 53 |
-
break
|
| 54 |
-
|
| 55 |
-
if not media:
|
| 56 |
-
abort(400, 'Invalid media type.')
|
| 57 |
-
|
| 58 |
date = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
| 59 |
file_name = f'{file_type}-{date}.{file_format}'
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
return file_name, file_size, mime_type
|
|
|
|
| 1 |
+
from hydrogram.types import Message
|
|
|
|
| 2 |
from datetime import datetime
|
| 3 |
from mimetypes import guess_type
|
| 4 |
from bot import TelegramBot
|
|
|
|
| 9 |
message = None
|
| 10 |
|
| 11 |
try:
|
| 12 |
+
message = await TelegramBot.get_messages(Telegram.CHANNEL_ID, message_ids=message_id)
|
| 13 |
+
if message.empty: message = None
|
| 14 |
except Exception:
|
| 15 |
pass
|
| 16 |
|
| 17 |
return message
|
| 18 |
|
| 19 |
+
async def send_message(msg: Message, send_to: int = Telegram.CHANNEL_ID) -> Message:
|
| 20 |
+
return await TelegramBot.send_message(entity=send_to, message=msg)
|
| 21 |
+
|
| 22 |
+
def get_file_properties(msg: Message):
|
| 23 |
+
attributes = (
|
| 24 |
+
'document',
|
| 25 |
+
'video',
|
| 26 |
+
'audio',
|
| 27 |
+
'voice',
|
| 28 |
+
'photo',
|
| 29 |
+
'video_note'
|
|
|
|
|
|
|
|
|
|
| 30 |
)
|
| 31 |
+
for attribute in attributes:
|
| 32 |
+
media = getattr(msg, attribute, None)
|
| 33 |
+
if media:
|
| 34 |
+
file_type = attribute
|
| 35 |
+
break
|
| 36 |
+
|
| 37 |
+
if not media: abort(400, 'Unknown file type.')
|
| 38 |
|
| 39 |
+
file_name = getattr(media, 'file_name', None)
|
| 40 |
+
file_size = getattr(media, 'file_size', 0)
|
|
|
|
|
|
|
| 41 |
|
| 42 |
if not file_name:
|
| 43 |
+
file_format = {
|
| 44 |
'video': 'mp4',
|
| 45 |
'audio': 'mp3',
|
| 46 |
'voice': 'ogg',
|
| 47 |
'photo': 'jpg',
|
| 48 |
'video_note': 'mp4'
|
| 49 |
+
}.get(file_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
date = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
| 51 |
file_name = f'{file_type}-{date}.{file_format}'
|
| 52 |
|
| 53 |
+
mime_type = guess_type(file_name)[0] or 'application/octet-stream'
|
| 54 |
+
|
|
|
|
| 55 |
return file_name, file_size, mime_type
|
bot/plugins/callback.py
CHANGED
|
@@ -1,24 +1,31 @@
|
|
| 1 |
-
from
|
| 2 |
from bot import TelegramBot
|
| 3 |
from bot.modules.decorators import verify_user
|
| 4 |
from bot.modules.static import *
|
| 5 |
from bot.modules.telegram import get_message
|
| 6 |
|
| 7 |
-
@TelegramBot.
|
| 8 |
-
@verify_user
|
| 9 |
-
async def
|
| 10 |
-
|
| 11 |
|
| 12 |
-
if
|
| 13 |
-
|
| 14 |
|
| 15 |
-
|
|
|
|
| 16 |
|
| 17 |
-
|
| 18 |
-
return await event.answer(MessageNotExist, alert=True)
|
| 19 |
-
if query_data[2] != message.raw_text:
|
| 20 |
-
return await event.answer(InvalidQueryText, alert=True)
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from hydrogram.types import CallbackQuery
|
| 2 |
from bot import TelegramBot
|
| 3 |
from bot.modules.decorators import verify_user
|
| 4 |
from bot.modules.static import *
|
| 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 |
|
| 12 |
+
if query.startswith('rm_'):
|
| 13 |
+
sq = query.split('_')
|
| 14 |
|
| 15 |
+
if len(sq) != 3:
|
| 16 |
+
return await q.answer(InvalidQueryText, show_alert=True)
|
| 17 |
|
| 18 |
+
message = await get_message(int(sq[1]))
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
if not message:
|
| 21 |
+
return await q.answer(MessageNotExist, show_alert=True)
|
| 22 |
+
|
| 23 |
+
sc = message.caption.split('/')
|
| 24 |
|
| 25 |
+
if q.from_user.id != int(sc[1]) or sq[2] != sc[0]:
|
| 26 |
+
return await q.answer(InvalidQueryText, show_alert=True)
|
| 27 |
+
|
| 28 |
+
await message.delete()
|
| 29 |
+
await q.answer(LinkRevokedText, show_alert=True)
|
| 30 |
+
else:
|
| 31 |
+
await q.answer(InvalidQueryText, show_alert=True)
|
bot/plugins/commands.py
CHANGED
|
@@ -1,28 +1,18 @@
|
|
| 1 |
-
from
|
| 2 |
-
from
|
| 3 |
-
from telethon.tl.custom.message import Message
|
| 4 |
from bot import TelegramBot
|
| 5 |
-
from bot.config import Telegram
|
| 6 |
from bot.modules.static import *
|
| 7 |
from bot.modules.decorators import verify_user
|
| 8 |
|
| 9 |
-
@TelegramBot.
|
| 10 |
-
@verify_user
|
| 11 |
-
async def
|
| 12 |
-
await
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
[
|
| 16 |
-
Button.url('Add to Channel', f'https://t.me/{Telegram.BOT_USERNAME}?startchannel&admin=post_messages+edit_messages+delete_messages')
|
| 17 |
-
]
|
| 18 |
-
]
|
| 19 |
)
|
| 20 |
|
| 21 |
-
@TelegramBot.
|
| 22 |
-
@verify_user
|
| 23 |
-
async def
|
| 24 |
-
await
|
| 25 |
-
|
| 26 |
-
@TelegramBot.on(NewMessage(chats=Telegram.OWNER_ID, incoming=True, pattern=r'^/log$'))
|
| 27 |
-
async def send_log(event: NewMessage.Event | Message):
|
| 28 |
-
await event.reply(file='event-log.txt')
|
|
|
|
| 1 |
+
from hydrogram import filters
|
| 2 |
+
from hydrogram.types import Message
|
|
|
|
| 3 |
from bot import TelegramBot
|
|
|
|
| 4 |
from bot.modules.static import *
|
| 5 |
from bot.modules.decorators import verify_user
|
| 6 |
|
| 7 |
+
@TelegramBot.on_message(filters.command(['start', 'help']) & filters.private)
|
| 8 |
+
@verify_user
|
| 9 |
+
async def start_command(_, msg: Message):
|
| 10 |
+
await msg.reply(
|
| 11 |
+
text=WelcomeText % {'first_name': msg.from_user.first_name},
|
| 12 |
+
quote=True
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
)
|
| 14 |
|
| 15 |
+
@TelegramBot.on_message(filters.command('privacy') & filters.private)
|
| 16 |
+
@verify_user
|
| 17 |
+
async def privacy_command(_, msg: Message):
|
| 18 |
+
await msg.reply(text=PrivacyText, quote=True)
|
|
|
|
|
|
|
|
|
|
|
|
bot/plugins/deeplinks.py
CHANGED
|
@@ -1,24 +1,25 @@
|
|
| 1 |
-
|
| 2 |
-
from telethon.tl.custom import Message
|
| 3 |
-
from bot import TelegramBot
|
| 4 |
-
from bot.modules.decorators import verify_user
|
| 5 |
-
from bot.modules.telegram import get_message, send_message
|
| 6 |
-
from bot.modules.static import *
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
message = await get_message(int(payload[1]))
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Currently not in use.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
# from hydrogram.types import Message
|
| 4 |
+
# from bot import TelegramBot
|
| 5 |
+
# from bot.modules.decorators import verify_user
|
| 6 |
+
# from bot.modules.telegram import get_message, send_message
|
| 7 |
+
# from bot.modules.static import *
|
| 8 |
|
| 9 |
+
# async def deeplinks(msg: Message, payload: str):
|
| 10 |
+
# if payload.startswith('file_'):
|
| 11 |
+
# sp = payload.split('_')
|
|
|
|
| 12 |
|
| 13 |
+
# if len(sp) != 3:
|
| 14 |
+
# return await msg.reply(InvalidPayloadText, quote=True)
|
| 15 |
+
|
| 16 |
+
# message = await get_message(int(sp[1]))
|
| 17 |
+
|
| 18 |
+
# if not message:
|
| 19 |
+
# return await msg.reply(MessageNotExist)
|
| 20 |
+
# if sp[2] != message.caption:
|
| 21 |
+
# return await msg.reply(InvalidPayloadText, quote=True)
|
| 22 |
+
|
| 23 |
+
# await message.copy(chat_id=msg.from_user.id, caption="")
|
| 24 |
+
# else:
|
| 25 |
+
# await msg.reply(InvalidPayloadText, quote=True)
|
bot/plugins/files.py
CHANGED
|
@@ -1,95 +1,60 @@
|
|
| 1 |
-
from
|
| 2 |
-
from
|
| 3 |
-
from telethon.errors import MessageAuthorRequiredError, MessageNotModifiedError, MessageIdInvalidError
|
| 4 |
-
from telethon.tl.custom import Message
|
| 5 |
from secrets import token_hex
|
| 6 |
from bot import TelegramBot
|
| 7 |
from bot.config import Telegram, Server
|
| 8 |
from bot.modules.decorators import verify_user
|
| 9 |
-
from bot.modules.telegram import send_message, filter_files
|
| 10 |
from bot.modules.static import *
|
| 11 |
|
| 12 |
-
@TelegramBot.
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
secret_code = token_hex(Telegram.SECRET_CODE_LENGTH)
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
await event.reply(
|
| 27 |
-
message= MediaLinksText % {'dl_link': dl_link, 'tg_link': tg_link, 'tg_link': tg_link, 'stream_link': stream_link},
|
| 28 |
-
buttons=[
|
| 29 |
-
[
|
| 30 |
-
Button.url('Download', dl_link),
|
| 31 |
-
Button.url('Stream', stream_link)
|
| 32 |
-
],
|
| 33 |
[
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
]
|
| 37 |
-
|
| 38 |
)
|
| 39 |
else:
|
| 40 |
-
await
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
Button.url('Download', dl_link),
|
| 45 |
-
Button.url('Get File', deep_link)
|
| 46 |
-
],
|
| 47 |
[
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
@TelegramBot.on(NewMessage(incoming=True, func=filter_files, forwards=False))
|
| 54 |
-
@verify_user()
|
| 55 |
-
async def channel_file_handler(event: NewMessage.Event | Message):
|
| 56 |
-
if event.raw_text and '#pass' in event.raw_text:
|
| 57 |
-
return
|
| 58 |
-
|
| 59 |
-
secret_code = token_hex(Telegram.SECRET_CODE_LENGTH)
|
| 60 |
-
event.message.text = f"`{secret_code}`"
|
| 61 |
-
message = await send_message(event.message)
|
| 62 |
-
message_id = message.id
|
| 63 |
-
|
| 64 |
-
dl_link = f"{Server.BASE_URL}/dl/{message_id}?code={secret_code}"
|
| 65 |
-
tg_link = f"{Server.BASE_URL}/file/{message_id}?code={secret_code}"
|
| 66 |
-
|
| 67 |
-
if (event.document and "video" in event.document.mime_type) or event.video:
|
| 68 |
-
stream_link = f"{Server.BASE_URL}/stream/{message_id}?code={secret_code}"
|
| 69 |
-
|
| 70 |
-
try:
|
| 71 |
-
await event.edit(
|
| 72 |
-
buttons=[
|
| 73 |
-
[Button.url("Download", dl_link), Button.url("Stream", stream_link)],
|
| 74 |
-
[Button.url("Get File", tg_link)],
|
| 75 |
]
|
| 76 |
)
|
| 77 |
-
|
| 78 |
-
MessageAuthorRequiredError,
|
| 79 |
-
MessageIdInvalidError,
|
| 80 |
-
MessageNotModifiedError,
|
| 81 |
-
):
|
| 82 |
-
pass
|
| 83 |
-
else:
|
| 84 |
-
try:
|
| 85 |
-
await event.edit(
|
| 86 |
-
buttons=[
|
| 87 |
-
[Button.url("Download", dl_link), Button.url("Get File", tg_link)]
|
| 88 |
-
]
|
| 89 |
-
)
|
| 90 |
-
except (
|
| 91 |
-
MessageAuthorRequiredError,
|
| 92 |
-
MessageIdInvalidError,
|
| 93 |
-
MessageNotModifiedError,
|
| 94 |
-
):
|
| 95 |
-
pass
|
|
|
|
| 1 |
+
from hydrogram import filters
|
| 2 |
+
from hydrogram.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 |
+
sender_id = msg.from_user.id
|
| 23 |
secret_code = token_hex(Telegram.SECRET_CODE_LENGTH)
|
| 24 |
+
file = await msg.copy(
|
| 25 |
+
chat_id=Telegram.CHANNEL_ID,
|
| 26 |
+
caption=f'||{secret_code}/{sender_id}||'
|
| 27 |
+
)
|
| 28 |
+
file_id = file.id
|
| 29 |
+
dl_link = f'{Server.BASE_URL}/dl/{file_id}?code={secret_code}'
|
| 30 |
|
| 31 |
+
if (msg.document and 'video' in msg.document.mime_type) or msg.video:
|
| 32 |
+
stream_link = f'{Server.BASE_URL}/stream/{file_id}?code={secret_code}'
|
| 33 |
+
await msg.reply(
|
| 34 |
+
text=MediaLinksText % {'dl_link': dl_link, 'stream_link': stream_link},
|
| 35 |
+
quote=True,
|
| 36 |
+
reply_markup=InlineKeyboardMarkup(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
[
|
| 38 |
+
[
|
| 39 |
+
InlineKeyboardButton('Download', url=dl_link),
|
| 40 |
+
InlineKeyboardButton('Stream', url=stream_link)
|
| 41 |
+
],
|
| 42 |
+
[
|
| 43 |
+
InlineKeyboardButton('Revoke', callback_data=f'rm_{file_id}_{secret_code}')
|
| 44 |
+
]
|
| 45 |
]
|
| 46 |
+
)
|
| 47 |
)
|
| 48 |
else:
|
| 49 |
+
await msg.reply(
|
| 50 |
+
text=FileLinksText % {'dl_link': dl_link},
|
| 51 |
+
quote=True,
|
| 52 |
+
reply_markup=InlineKeyboardMarkup(
|
|
|
|
|
|
|
|
|
|
| 53 |
[
|
| 54 |
+
[
|
| 55 |
+
InlineKeyboardButton('Download', url=dl_link),
|
| 56 |
+
InlineKeyboardButton('Revoke', callback_data=f'rm_{file_id}_{secret_code}')
|
| 57 |
+
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
]
|
| 59 |
)
|
| 60 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bot/server/__init__.py
CHANGED
|
@@ -2,12 +2,12 @@ from quart import Quart
|
|
| 2 |
from uvicorn import Server as UvicornServer, Config
|
| 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 |
instance.config['RESPONSE_TIMEOUT'] = None
|
|
|
|
| 11 |
|
| 12 |
@instance.before_serving
|
| 13 |
async def before_serve():
|
|
|
|
| 2 |
from uvicorn import Server as UvicornServer, Config
|
| 3 |
from logging import getLogger
|
| 4 |
from bot.config import Server, LOGGER_CONFIG_JSON
|
|
|
|
| 5 |
from . import main, error
|
| 6 |
|
| 7 |
logger = getLogger('uvicorn')
|
| 8 |
instance = Quart(__name__)
|
| 9 |
instance.config['RESPONSE_TIMEOUT'] = None
|
| 10 |
+
instance.config['MAX_CONTENT_LENGTH'] = 999999999999999
|
| 11 |
|
| 12 |
@instance.before_serving
|
| 13 |
async def before_serve():
|
bot/server/main.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
from quart import Blueprint, Response, request, render_template, redirect
|
|
|
|
|
|
|
| 2 |
from .error import abort
|
| 3 |
from bot import TelegramBot
|
| 4 |
from bot.config import Telegram, Server
|
| 5 |
-
from math import ceil, floor
|
| 6 |
from bot.modules.telegram import get_message, get_file_properties
|
| 7 |
|
| 8 |
bp = Blueprint('main', __name__)
|
|
@@ -13,73 +14,70 @@ async def home():
|
|
| 13 |
|
| 14 |
@bp.route('/dl/<int:file_id>')
|
| 15 |
async def transmit_file(file_id):
|
| 16 |
-
file = await get_message(
|
| 17 |
code = request.args.get('code') or abort(401)
|
| 18 |
-
range_header = request.headers.get('Range'
|
| 19 |
|
| 20 |
-
if code != file.
|
| 21 |
abort(403)
|
| 22 |
|
| 23 |
file_name, file_size, mime_type = get_file_properties(file)
|
| 24 |
-
|
| 25 |
-
if range_header:
|
| 26 |
-
from_bytes, until_bytes = range_header.replace("bytes=", "").split("-")
|
| 27 |
-
from_bytes = int(from_bytes)
|
| 28 |
-
until_bytes = int(until_bytes) if until_bytes else file_size - 1
|
| 29 |
-
else:
|
| 30 |
-
from_bytes = 0
|
| 31 |
-
until_bytes = file_size - 1
|
| 32 |
-
|
| 33 |
-
if (until_bytes > file_size) or (from_bytes < 0) or (until_bytes < from_bytes):
|
| 34 |
-
abort(416, 'Invalid range.')
|
| 35 |
-
|
| 36 |
-
chunk_size = 1024 * 1024
|
| 37 |
-
until_bytes = min(until_bytes, file_size - 1)
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
headers = {
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
break
|
| 59 |
-
elif part_count == 1:
|
| 60 |
-
yield chunk[first_part_cut:last_part_cut]
|
| 61 |
-
elif current_part == 1:
|
| 62 |
-
yield chunk[first_part_cut:]
|
| 63 |
-
elif current_part == part_count:
|
| 64 |
-
yield chunk[:last_part_cut]
|
| 65 |
-
else:
|
| 66 |
-
yield chunk
|
| 67 |
|
| 68 |
-
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
|
|
|
| 72 |
|
| 73 |
-
return Response(
|
| 74 |
|
| 75 |
@bp.route('/stream/<int:file_id>')
|
| 76 |
async def stream_file(file_id):
|
| 77 |
code = request.args.get('code') or abort(401)
|
| 78 |
-
|
| 79 |
return await render_template('player.html', mediaLink=f'{Server.BASE_URL}/dl/{file_id}?code={code}')
|
| 80 |
-
|
| 81 |
-
@bp.route('/file/<int:file_id>')
|
| 82 |
-
async def file_deeplink(file_id):
|
| 83 |
-
code = request.args.get('code') or abort(401)
|
| 84 |
-
|
| 85 |
-
return redirect(f'https://t.me/{Telegram.BOT_USERNAME}?start=file_{file_id}_{code}')
|
|
|
|
| 1 |
from quart import Blueprint, Response, request, render_template, redirect
|
| 2 |
+
from math import ceil
|
| 3 |
+
from re import match as re_match
|
| 4 |
from .error import abort
|
| 5 |
from bot import TelegramBot
|
| 6 |
from bot.config import Telegram, Server
|
|
|
|
| 7 |
from bot.modules.telegram import get_message, get_file_properties
|
| 8 |
|
| 9 |
bp = Blueprint('main', __name__)
|
|
|
|
| 14 |
|
| 15 |
@bp.route('/dl/<int:file_id>')
|
| 16 |
async def transmit_file(file_id):
|
| 17 |
+
file = await get_message(file_id) or abort(404)
|
| 18 |
code = request.args.get('code') or abort(401)
|
| 19 |
+
range_header = request.headers.get('Range')
|
| 20 |
|
| 21 |
+
if code != file.caption.split('/')[0]:
|
| 22 |
abort(403)
|
| 23 |
|
| 24 |
file_name, file_size, mime_type = get_file_properties(file)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
start = 0
|
| 27 |
+
end = file_size - 1
|
| 28 |
+
chunk_size = 1 * 1024 * 1024 # 1 MB
|
| 29 |
|
| 30 |
+
if range_header:
|
| 31 |
+
range_match = re_match(r'bytes=(\d+)-(\d*)', range_header)
|
| 32 |
+
if range_match:
|
| 33 |
+
start = int(range_match.group(1))
|
| 34 |
+
end = int(range_match.group(2)) if range_match.group(2) else file_size - 1
|
| 35 |
+
if start > end or start >= file_size:
|
| 36 |
+
abort(416, 'Requested range not satisfiable')
|
| 37 |
+
else:
|
| 38 |
+
abort(400, 'Invalid Range header')
|
| 39 |
+
|
| 40 |
+
offset_chunks = start // chunk_size
|
| 41 |
+
total_bytes_to_stream = end - start + 1
|
| 42 |
+
chunks_to_stream = ceil(total_bytes_to_stream / chunk_size)
|
| 43 |
+
|
| 44 |
+
content_length = total_bytes_to_stream
|
| 45 |
headers = {
|
| 46 |
+
'Content-Type': mime_type,
|
| 47 |
+
'Content-Disposition': f'attachment; filename={file_name}',
|
| 48 |
+
'Content-Range': f'bytes {start}-{end}/{file_size}',
|
| 49 |
+
'Accept-Ranges': 'bytes',
|
| 50 |
+
'Content-Length': str(content_length),
|
| 51 |
+
}
|
| 52 |
+
status_code = 206 if range_header else 200
|
| 53 |
+
|
| 54 |
+
async def file_stream():
|
| 55 |
+
bytes_streamed = 0
|
| 56 |
+
chunk_index = 0
|
| 57 |
+
async for chunk in TelegramBot.stream_media(
|
| 58 |
+
file,
|
| 59 |
+
offset=offset_chunks,
|
| 60 |
+
limit=chunks_to_stream,
|
| 61 |
+
):
|
| 62 |
+
if chunk_index == 0: # Trim the first chunk if necessary
|
| 63 |
+
trim_start = start % chunk_size
|
| 64 |
+
if trim_start > 0:
|
| 65 |
+
chunk = chunk[trim_start:]
|
| 66 |
+
|
| 67 |
+
remaining_bytes = content_length - bytes_streamed
|
| 68 |
+
if remaining_bytes <= 0:
|
| 69 |
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
if len(chunk) > remaining_bytes: # Trim the last chunk if necessary
|
| 72 |
+
chunk = chunk[:remaining_bytes]
|
| 73 |
|
| 74 |
+
yield chunk
|
| 75 |
+
bytes_streamed += len(chunk)
|
| 76 |
+
chunk_index += 1
|
| 77 |
|
| 78 |
+
return Response(file_stream(), headers=headers, status=status_code)
|
| 79 |
|
| 80 |
@bp.route('/stream/<int:file_id>')
|
| 81 |
async def stream_file(file_id):
|
| 82 |
code = request.args.get('code') or abort(401)
|
|
|
|
| 83 |
return await render_template('player.html', mediaLink=f'{Server.BASE_URL}/dl/{file_id}?code={code}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
quart
|
| 4 |
uvicorn
|
|
|
|
| 1 |
+
hydrogram
|
| 2 |
+
tgcrypto
|
| 3 |
quart
|
| 4 |
uvicorn
|