mirror of
https://github.com/anonymousx97/social-dl.git
synced 2025-02-20 11:13:19 +08:00
v2.5.0:Upstream with Private Repo.
What's New: - Sort Plugins into their respective folders. - download, upload, rename commands. - Help Docstrings. - Proper Logging. - rename .exec cmd to .py
This commit is contained in:
parent
cdf7ca73df
commit
31d19bc48b
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
config.env
|
config.env
|
||||||
.idea
|
.idea
|
||||||
|
logs/
|
||||||
|
.vscode
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import os
|
import os
|
||||||
|
import tracemalloc
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv("config.env")
|
load_dotenv("config.env")
|
||||||
|
tracemalloc.start()
|
||||||
|
|
||||||
|
|
||||||
# isort: skip
|
# isort: skip
|
||||||
from .config import Config # noqa
|
from app.config import Config # NOQA
|
||||||
from app.core.message import Message # noqa
|
from app.core import LOGGER, Message # NOQA
|
||||||
from .core.client import BOT # noqa
|
from app.core.client.client import BOT # NOQA
|
||||||
|
|
||||||
if "com.termux" not in os.environ.get("PATH", ""):
|
if "com.termux" not in os.environ.get("PATH", ""):
|
||||||
import uvloop # isort:skip
|
import uvloop # isort:skip
|
||||||
|
|
||||||
uvloop.install()
|
uvloop.install()
|
||||||
|
|
||||||
bot = BOT()
|
bot: BOT = BOT()
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
|
from app import LOGGER, bot
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import tracemalloc
|
|
||||||
|
|
||||||
tracemalloc.start()
|
|
||||||
|
|
||||||
from app import bot
|
|
||||||
|
|
||||||
bot.run(bot.boot())
|
bot.run(bot.boot())
|
||||||
|
else:
|
||||||
|
LOGGER.error("Wrong Start Command.\nUse 'python -m app'")
|
||||||
|
@ -1,112 +0,0 @@
|
|||||||
import os
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from app import Config, Message, bot
|
|
||||||
from app.core.aiohttp_tools import get_json, get_type
|
|
||||||
from app.core.scraper_config import MediaType, ScraperConfig
|
|
||||||
|
|
||||||
API_KEYS = {"KEYS": Config.API_KEYS, "counter": 0}
|
|
||||||
|
|
||||||
|
|
||||||
async def get_key():
|
|
||||||
keys, count = API_KEYS.values()
|
|
||||||
count += 1
|
|
||||||
if count == len(keys):
|
|
||||||
count = 0
|
|
||||||
ret_key = keys[count]
|
|
||||||
API_KEYS["counter"] = count
|
|
||||||
return ret_key
|
|
||||||
|
|
||||||
|
|
||||||
class Instagram(ScraperConfig):
|
|
||||||
def __init__(self, url):
|
|
||||||
super().__init__()
|
|
||||||
self.shortcode = os.path.basename(urlparse(url).path.rstrip("/"))
|
|
||||||
self.api_url = (f"https://www.instagram.com/graphql/query?query_hash=2b0673e0dc4580674a88d426fe00ea90"
|
|
||||||
f"&variables=%7B%22shortcode%22%3A%22{self.shortcode}%22%7D")
|
|
||||||
self.url = url
|
|
||||||
self.dump = True
|
|
||||||
|
|
||||||
async def check_dump(self) -> None | bool:
|
|
||||||
if not Config.DUMP_ID:
|
|
||||||
return
|
|
||||||
async for message in bot.search_messages(Config.DUMP_ID, "#" + self.shortcode):
|
|
||||||
self.media: Message = message
|
|
||||||
self.type: MediaType = MediaType.MESSAGE
|
|
||||||
self.in_dump: bool = True
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def download_or_extract(self):
|
|
||||||
for func in [self.check_dump, self.api_3, self.no_api_dl, self.api_dl]:
|
|
||||||
if await func():
|
|
||||||
self.success: bool = True
|
|
||||||
break
|
|
||||||
|
|
||||||
async def api_3(self):
|
|
||||||
query_api = f"https://{bot.SECRET_API}?url={self.url}&v=1"
|
|
||||||
response = await get_json(url=query_api, json_=False)
|
|
||||||
if not response:
|
|
||||||
return
|
|
||||||
self.caption = "."
|
|
||||||
data: list = (
|
|
||||||
response.get("videos", [])
|
|
||||||
+ response.get("images", [])
|
|
||||||
+ response.get("stories", [])
|
|
||||||
)
|
|
||||||
if not data:
|
|
||||||
return
|
|
||||||
if len(data) > 1:
|
|
||||||
self.type = MediaType.GROUP
|
|
||||||
self.media: list = data
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.media: str = data[0]
|
|
||||||
self.type: MediaType = get_type(self.media)
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def no_api_dl(self):
|
|
||||||
response = await get_json(url=self.api_url)
|
|
||||||
if (
|
|
||||||
not response
|
|
||||||
or "data" not in response
|
|
||||||
or not response["data"]["shortcode_media"]
|
|
||||||
):
|
|
||||||
return
|
|
||||||
return await self.parse_ghraphql(response["data"]["shortcode_media"])
|
|
||||||
|
|
||||||
async def api_dl(self):
|
|
||||||
if not Config.API_KEYS:
|
|
||||||
return
|
|
||||||
param = {
|
|
||||||
"api_key": await get_key(),
|
|
||||||
"url": self.api_url,
|
|
||||||
"proxy": "residential",
|
|
||||||
"js": "false",
|
|
||||||
}
|
|
||||||
response: dict | None = await get_json(
|
|
||||||
url="https://api.webscraping.ai/html", timeout=30, params=param
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
not response
|
|
||||||
or "data" not in response
|
|
||||||
or not response["data"]["shortcode_media"]
|
|
||||||
):
|
|
||||||
return
|
|
||||||
self.caption = ".."
|
|
||||||
return await self.parse_ghraphql(response["data"]["shortcode_media"])
|
|
||||||
|
|
||||||
async def parse_ghraphql(self, json_: dict) -> str | list | None:
|
|
||||||
type_check: str | None = json_.get("__typename", None)
|
|
||||||
if not type_check:
|
|
||||||
return
|
|
||||||
elif type_check == "GraphSidecar":
|
|
||||||
self.media: list[str] = [
|
|
||||||
i["node"].get("video_url") or i["node"].get("display_url")
|
|
||||||
for i in json_["edge_sidecar_to_children"]["edges"]
|
|
||||||
]
|
|
||||||
self.type: MediaType = MediaType.GROUP
|
|
||||||
else:
|
|
||||||
self.media: str = json_.get("video_url", json_.get("display_url"))
|
|
||||||
self.thumb: str = json_.get("display_url")
|
|
||||||
self.type: MediaType = get_type(self.media)
|
|
||||||
return self.media
|
|
@ -1,37 +1,57 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
from pyrogram.filters import Filter
|
from git import Repo
|
||||||
from pyrogram.types import Message
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
class _Config:
|
||||||
API_KEYS: list[int] = json.loads(os.environ.get("API_KEYS", "[]"))
|
class CMD:
|
||||||
|
def __init__(self, func, path, doc):
|
||||||
|
self.func = func
|
||||||
|
self.path = path
|
||||||
|
self.doc = doc or "Not Documented."
|
||||||
|
|
||||||
BLOCKED_USERS: list[int] = []
|
def __init__(self):
|
||||||
BLOCKED_USERS_MESSAGE_ID: int = int(os.environ.get("BLOCKED_USERS_MESSAGE_ID", 0))
|
self.API_KEYS: list[int] = json.loads(os.environ.get("API_KEYS", "[]"))
|
||||||
|
|
||||||
CHATS: list[int] = []
|
self.BLOCKED_USERS: list[int] = []
|
||||||
AUTO_DL_MESSAGE_ID: int = int(os.environ.get("AUTO_DL_MESSAGE_ID", 0))
|
self.BLOCKED_USERS_MESSAGE_ID: int = int(
|
||||||
|
os.environ.get("BLOCKED_USERS_MESSAGE_ID", 0)
|
||||||
|
)
|
||||||
|
|
||||||
CMD_DICT: dict[str, Callable] = {}
|
self.CHATS: list[int] = []
|
||||||
CONVO_DICT: dict[int, dict[str | int, Message | Filter | None]] = {}
|
self.AUTO_DL_MESSAGE_ID: int = int(os.environ.get("AUTO_DL_MESSAGE_ID", 0))
|
||||||
|
|
||||||
DEV_MODE: int = int(os.environ.get("DEV_MODE", 0))
|
self.CMD_TRIGGER: str = os.environ.get("TRIGGER", ".")
|
||||||
|
|
||||||
DISABLED_CHATS: list[int] = []
|
self.CMD_DICT: dict[str, _Config.CMD] = {}
|
||||||
DISABLED_CHATS_MESSAGE_ID: int = int(os.environ.get("DISABLED_CHATS_MESSAGE_ID", 0))
|
|
||||||
|
|
||||||
DUMP_ID: int = int(os.environ.get("DUMP_ID", 0))
|
self.DEV_MODE: int = int(os.environ.get("DEV_MODE", 0))
|
||||||
|
|
||||||
LOG_CHAT: int = int(os.environ.get("LOG_CHAT"))
|
self.DISABLED_CHATS: list[int] = []
|
||||||
|
|
||||||
TRIGGER: str = os.environ.get("TRIGGER", ".")
|
self.DISABLED_CHATS_MESSAGE_ID: int = int(
|
||||||
|
os.environ.get("DISABLED_CHATS_MESSAGE_ID", 0)
|
||||||
|
)
|
||||||
|
|
||||||
UPSTREAM_REPO = os.environ.get(
|
self.DUMP_ID: int = int(os.environ.get("DUMP_ID", 0))
|
||||||
|
|
||||||
|
self.INIT_TASKS: list = []
|
||||||
|
|
||||||
|
self.LOG_CHAT: int = int(os.environ.get("LOG_CHAT"))
|
||||||
|
|
||||||
|
self.REPO = Repo(".")
|
||||||
|
|
||||||
|
self.UPSTREAM_REPO = os.environ.get(
|
||||||
"UPSTREAM_REPO", "https://github.com/anonymousx97/social-dl"
|
"UPSTREAM_REPO", "https://github.com/anonymousx97/social-dl"
|
||||||
).rstrip("/")
|
).rstrip("/")
|
||||||
|
|
||||||
USERS: list[int] = []
|
self.USERS: list[int] = []
|
||||||
USERS_MESSAGE_ID: int = int(os.environ.get("USERS_MESSAGE_ID", 0))
|
self.USERS_MESSAGE_ID: int = int(os.environ.get("USERS_MESSAGE_ID", 0))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
config_dict = self.__dict__.copy()
|
||||||
|
return json.dumps(config_dict, indent=4, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
Config = _Config()
|
||||||
|
3
app/core/__init__.py
Normal file
3
app/core/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from app.core.types.message import Message # NOQA
|
||||||
|
|
||||||
|
from app.core.logger import LOGGER # NOQA
|
@ -1,29 +1,32 @@
|
|||||||
|
import asyncio
|
||||||
import glob
|
import glob
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from pyrogram import Client, filters, idle
|
from pyrogram import Client, idle
|
||||||
from pyrogram.enums import ParseMode
|
from pyrogram.enums import ParseMode
|
||||||
|
from pyrogram.filters import Filter
|
||||||
from pyrogram.types import Message as Msg
|
from pyrogram.types import Message as Msg
|
||||||
|
|
||||||
from app import Config
|
from app import LOGGER, Config, Message
|
||||||
from app.core import aiohttp_tools
|
from app.utils import aiohttp_tools
|
||||||
from app.core.conversation import Conversation
|
|
||||||
from app.core.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
async def import_modules():
|
def import_modules():
|
||||||
for py_module in glob.glob(pathname="app/**/*.py", recursive=True):
|
for py_module in glob.glob(pathname="app/**/[!^_]*.py", recursive=True):
|
||||||
name = os.path.splitext(py_module)[0]
|
name = os.path.splitext(py_module)[0]
|
||||||
py_name = name.replace("/", ".")
|
py_name = name.replace("/", ".")
|
||||||
try:
|
try:
|
||||||
importlib.import_module(py_name)
|
mod = importlib.import_module(py_name)
|
||||||
except Exception as e:
|
if hasattr(mod, "init_task"):
|
||||||
print(e)
|
Config.INIT_TASKS.append(mod.init_task())
|
||||||
|
except BaseException:
|
||||||
|
LOGGER.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
class BOT(Client):
|
class BOT(Client):
|
||||||
@ -42,59 +45,56 @@ class BOT(Client):
|
|||||||
sleep_threshold=30,
|
sleep_threshold=30,
|
||||||
max_concurrent_transmissions=2,
|
max_concurrent_transmissions=2,
|
||||||
)
|
)
|
||||||
|
from app.core.client.conversation import Conversation
|
||||||
|
|
||||||
|
self.Convo = Conversation
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_cmd(cmd: str):
|
def add_cmd(cmd: str | list):
|
||||||
def the_decorator(func):
|
def the_decorator(func):
|
||||||
|
path = inspect.stack()[1][1]
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper():
|
def wrapper():
|
||||||
if isinstance(cmd, list):
|
if isinstance(cmd, list):
|
||||||
for _cmd in cmd:
|
for _cmd in cmd:
|
||||||
Config.CMD_DICT[_cmd] = func
|
Config.CMD_DICT[_cmd] = Config.CMD(
|
||||||
|
func=func, path=path, doc=func.__doc__
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
Config.CMD_DICT[cmd] = func
|
Config.CMD_DICT[cmd] = Config.CMD(
|
||||||
|
func=func, path=path, doc=func.__doc__
|
||||||
|
)
|
||||||
|
|
||||||
wrapper()
|
wrapper()
|
||||||
return func
|
return func
|
||||||
|
|
||||||
return the_decorator
|
return the_decorator
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_response(
|
async def get_response(
|
||||||
chat_id: int, filters: filters.Filter = None, timeout: int = 8
|
self, chat_id: int, filters: Filter = None, timeout: int = 8
|
||||||
) -> Message | None:
|
) -> Message | None:
|
||||||
try:
|
try:
|
||||||
async with Conversation(
|
async with self.Convo(
|
||||||
chat_id=chat_id, filters=filters, timeout=timeout
|
chat_id=chat_id, filters=filters, timeout=timeout
|
||||||
) as convo:
|
) as convo:
|
||||||
response: Message | None = await convo.get_response()
|
response: Message | None = await convo.get_response()
|
||||||
return response
|
return response
|
||||||
except Conversation.TimeOutError:
|
except TimeoutError:
|
||||||
return
|
return
|
||||||
|
|
||||||
async def boot(self) -> None:
|
async def boot(self) -> None:
|
||||||
await super().start()
|
await super().start()
|
||||||
await import_modules()
|
LOGGER.info("Started.")
|
||||||
await self.set_filter_list()
|
import_modules()
|
||||||
await aiohttp_tools.session_switch()
|
LOGGER.info("Plugins Imported.")
|
||||||
await self.edit_restart_msg()
|
await asyncio.gather(*Config.INIT_TASKS)
|
||||||
await self.log(text="#SocialDL\n<i>Started</i>")
|
Config.INIT_TASKS.clear()
|
||||||
print("started")
|
LOGGER.info("Init Tasks Completed.")
|
||||||
|
await self.log(text="<i>Started</i>")
|
||||||
|
LOGGER.info("Idling...")
|
||||||
await idle()
|
await idle()
|
||||||
await aiohttp_tools.session_switch()
|
await aiohttp_tools.init_task()
|
||||||
|
|
||||||
async def edit_restart_msg(self) -> None:
|
|
||||||
restart_msg = int(os.environ.get("RESTART_MSG", 0))
|
|
||||||
restart_chat = int(os.environ.get("RESTART_CHAT", 0))
|
|
||||||
if restart_msg and restart_chat:
|
|
||||||
await super().get_chat(restart_chat)
|
|
||||||
await super().edit_message_text(
|
|
||||||
chat_id=restart_chat,
|
|
||||||
message_id=restart_msg,
|
|
||||||
text="#Social-dl\n__Started__",
|
|
||||||
)
|
|
||||||
os.environ.pop("RESTART_MSG", "")
|
|
||||||
os.environ.pop("RESTART_CHAT", "")
|
|
||||||
|
|
||||||
async def log(
|
async def log(
|
||||||
self,
|
self,
|
||||||
@ -110,27 +110,29 @@ class BOT(Client):
|
|||||||
if message:
|
if message:
|
||||||
return (await message.copy(chat_id=Config.LOG_CHAT)) # fmt: skip
|
return (await message.copy(chat_id=Config.LOG_CHAT)) # fmt: skip
|
||||||
if traceback:
|
if traceback:
|
||||||
text = f"""
|
text = (
|
||||||
#Traceback
|
"#Traceback"
|
||||||
<b>Function:</b> {func}
|
f"\n<b>Function:</b> {func}"
|
||||||
<b>Chat:</b> {chat}
|
f"\n<b>Chat:</b> {chat}"
|
||||||
<b>Traceback:</b>
|
f"\n<b>Traceback:</b>"
|
||||||
<code>{traceback}</code>"""
|
f"\n<code>{traceback}</code>"
|
||||||
return await self.send_message(
|
)
|
||||||
|
return (await self.send_message(
|
||||||
chat_id=Config.LOG_CHAT,
|
chat_id=Config.LOG_CHAT,
|
||||||
text=text,
|
text=text,
|
||||||
name=name,
|
name=name,
|
||||||
disable_web_page_preview=disable_web_page_preview,
|
disable_web_page_preview=disable_web_page_preview,
|
||||||
parse_mode=parse_mode,
|
parse_mode=parse_mode,
|
||||||
)
|
)) # fmt:skip
|
||||||
|
|
||||||
async def restart(self, hard=False) -> None:
|
async def restart(self, hard=False) -> None:
|
||||||
await aiohttp_tools.session_switch()
|
await aiohttp_tools.init_task()
|
||||||
await super().stop(block=False)
|
await super().stop(block=False)
|
||||||
if hard:
|
if hard:
|
||||||
|
os.remove("logs/app_logs.txt")
|
||||||
os.execl("/bin/bash", "/bin/bash", "run")
|
os.execl("/bin/bash", "/bin/bash", "run")
|
||||||
|
LOGGER.info("Restarting......")
|
||||||
os.execl(sys.executable, sys.executable, "-m", "app")
|
os.execl(sys.executable, sys.executable, "-m", "app")
|
||||||
SECRET_API = base64.b64decode("YS56dG9yci5tZS9hcGkvaW5zdGE=").decode("utf-8")
|
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
self, chat_id: int | str, text, name: str = "output.txt", **kwargs
|
self, chat_id: int | str, text, name: str = "output.txt", **kwargs
|
||||||
@ -144,26 +146,3 @@ class BOT(Client):
|
|||||||
doc.name = name
|
doc.name = name
|
||||||
kwargs.pop("disable_web_page_preview", "")
|
kwargs.pop("disable_web_page_preview", "")
|
||||||
return await super().send_document(chat_id=chat_id, document=doc, **kwargs)
|
return await super().send_document(chat_id=chat_id, document=doc, **kwargs)
|
||||||
|
|
||||||
async def set_filter_list(self):
|
|
||||||
chats_id = Config.AUTO_DL_MESSAGE_ID
|
|
||||||
blocked_id = Config.BLOCKED_USERS_MESSAGE_ID
|
|
||||||
users = Config.USERS_MESSAGE_ID
|
|
||||||
disabled = Config.DISABLED_CHATS_MESSAGE_ID
|
|
||||||
|
|
||||||
if chats_id:
|
|
||||||
Config.CHATS = json.loads(
|
|
||||||
(await super().get_messages(Config.LOG_CHAT, chats_id)).text
|
|
||||||
)
|
|
||||||
if blocked_id:
|
|
||||||
Config.BLOCKED_USERS = json.loads(
|
|
||||||
(await super().get_messages(Config.LOG_CHAT, blocked_id)).text
|
|
||||||
)
|
|
||||||
if users:
|
|
||||||
Config.USERS = json.loads(
|
|
||||||
(await super().get_messages(Config.LOG_CHAT, users)).text
|
|
||||||
)
|
|
||||||
if disabled:
|
|
||||||
Config.DISABLED_CHATS = json.loads(
|
|
||||||
(await super().get_messages(Config.LOG_CHAT, disabled)).text
|
|
||||||
)
|
|
93
app/core/client/conversation.py
Normal file
93
app/core/client/conversation.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pyrogram.filters import Filter
|
||||||
|
from pyrogram.types import Message
|
||||||
|
|
||||||
|
|
||||||
|
class Conversation:
|
||||||
|
CONVO_DICT: dict[int, "Conversation"] = {}
|
||||||
|
|
||||||
|
class DuplicateConvo(Exception):
|
||||||
|
def __init__(self, chat: str | int):
|
||||||
|
super().__init__(f"Conversation already started with {chat} ")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, chat_id: int | str, filters: Filter | None = None, timeout: int = 10
|
||||||
|
):
|
||||||
|
self.chat_id = chat_id
|
||||||
|
self.filters = filters
|
||||||
|
self.timeout = timeout
|
||||||
|
self.responses: list = []
|
||||||
|
self.set_future()
|
||||||
|
from app import bot
|
||||||
|
|
||||||
|
self._client = bot
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return json.dumps(self.__dict__, indent=4, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
def set_future(self, *args, **kwargs):
|
||||||
|
future = asyncio.Future()
|
||||||
|
future.add_done_callback(self.set_future)
|
||||||
|
self.response = future
|
||||||
|
|
||||||
|
async def get_response(self, timeout: int | None = None) -> Message | None:
|
||||||
|
try:
|
||||||
|
resp_future: asyncio.Future.result = await asyncio.wait_for(
|
||||||
|
self.response, timeout=timeout or self.timeout
|
||||||
|
)
|
||||||
|
return resp_future
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise TimeoutError("Conversation Timeout")
|
||||||
|
|
||||||
|
async def send_message(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
timeout=0,
|
||||||
|
get_response=False,
|
||||||
|
**kwargs,
|
||||||
|
) -> Message | tuple[Message, Message]:
|
||||||
|
message = await self._client.send_message(
|
||||||
|
chat_id=self.chat_id, text=text, **kwargs
|
||||||
|
)
|
||||||
|
if get_response:
|
||||||
|
response = await self.get_response(timeout=timeout or self.timeout)
|
||||||
|
return message, response
|
||||||
|
return message
|
||||||
|
|
||||||
|
async def send_document(
|
||||||
|
self,
|
||||||
|
document,
|
||||||
|
caption="",
|
||||||
|
timeout=0,
|
||||||
|
get_response=False,
|
||||||
|
**kwargs,
|
||||||
|
) -> Message | tuple[Message, Message]:
|
||||||
|
message = await self._client.send_document(
|
||||||
|
chat_id=self.chat_id,
|
||||||
|
document=document,
|
||||||
|
caption=caption,
|
||||||
|
force_document=True,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
if get_response:
|
||||||
|
response = await self.get_response(timeout=timeout or self.timeout)
|
||||||
|
return message, response
|
||||||
|
return message
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "Conversation":
|
||||||
|
if isinstance(self.chat_id, str):
|
||||||
|
self.chat_id = (await self._client.get_chat(self.chat_id)).id
|
||||||
|
if (
|
||||||
|
self.chat_id in Conversation.CONVO_DICT.keys()
|
||||||
|
and Conversation.CONVO_DICT[self.chat_id].filters == self.filters
|
||||||
|
):
|
||||||
|
raise self.DuplicateConvo(self.chat_id)
|
||||||
|
Conversation.CONVO_DICT[self.chat_id] = self
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
Conversation.CONVO_DICT.pop(self.chat_id, None)
|
||||||
|
if not self.response.done():
|
||||||
|
self.response.cancel()
|
@ -4,11 +4,19 @@ from pyrogram import filters as _filters
|
|||||||
from pyrogram.types import Message
|
from pyrogram.types import Message
|
||||||
|
|
||||||
from app import Config
|
from app import Config
|
||||||
from app.core.media_handler import url_map
|
from app.core.client.conversation import Conversation
|
||||||
|
from app.social_dl.media_handler import url_map
|
||||||
|
|
||||||
|
convo_filter = _filters.create(
|
||||||
|
lambda _, __, message: (message.chat.id in Conversation.CONVO_DICT.keys())
|
||||||
|
and (not message.reactions)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_for_urls(text_list: list):
|
def check_for_urls(text_list: list):
|
||||||
for link in text_list:
|
for link in text_list:
|
||||||
|
if "music.youtube.com" in link:
|
||||||
|
continue
|
||||||
if url_map.get(urlparse(link).netloc):
|
if url_map.get(urlparse(link).netloc):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
@ -35,35 +43,31 @@ def dynamic_chat_filter(_, __, message: Message, cmd=False) -> bool:
|
|||||||
return bool(url_check)
|
return bool(url_check)
|
||||||
|
|
||||||
|
|
||||||
|
chat_filter = _filters.create(dynamic_chat_filter)
|
||||||
|
|
||||||
|
|
||||||
def dynamic_allowed_list(_, __, message: Message) -> bool:
|
def dynamic_allowed_list(_, __, message: Message) -> bool:
|
||||||
if not dynamic_chat_filter(_, __, message, cmd=True):
|
if message.reactions or not dynamic_chat_filter(_, __, message, cmd=True):
|
||||||
return False
|
return False
|
||||||
cmd = message.text.split(maxsplit=1)[0]
|
cmd = message.text.split(maxsplit=1)[0]
|
||||||
cmd_check = cmd in {"/download", "/dl", "/down"}
|
return cmd in {"/download", "/dl", "/down"}
|
||||||
reaction_check = not message.reactions
|
|
||||||
return bool(cmd_check and reaction_check)
|
|
||||||
|
allowed_cmd_filter = _filters.create(dynamic_allowed_list)
|
||||||
|
|
||||||
|
|
||||||
def dynamic_cmd_filter(_, __, message: Message) -> bool:
|
def dynamic_cmd_filter(_, __, message: Message) -> bool:
|
||||||
if (
|
if (
|
||||||
not message.text
|
message.reactions
|
||||||
or not message.text.startswith(Config.TRIGGER)
|
or not message.text
|
||||||
|
or not message.text.startswith(Config.CMD_TRIGGER)
|
||||||
or not message.from_user
|
or not message.from_user
|
||||||
or message.from_user.id not in Config.USERS
|
or message.from_user.id not in Config.USERS
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
start_str = message.text.split(maxsplit=1)[0]
|
start_str = message.text.split(maxsplit=1)[0]
|
||||||
cmd = start_str.replace(Config.TRIGGER, "", 1)
|
cmd = start_str.replace(Config.CMD_TRIGGER, "", 1)
|
||||||
cmd_check = cmd in Config.CMD_DICT
|
return cmd in Config.CMD_DICT.keys()
|
||||||
reaction_check = not message.reactions
|
|
||||||
return bool(cmd_check and reaction_check)
|
|
||||||
|
|
||||||
|
|
||||||
chat_filter = _filters.create(dynamic_chat_filter)
|
|
||||||
user_filter = _filters.create(dynamic_cmd_filter)
|
user_filter = _filters.create(dynamic_cmd_filter)
|
||||||
allowed_cmd_filter = _filters.create(dynamic_allowed_list)
|
|
||||||
convo_filter = _filters.create(
|
|
||||||
lambda _, __, message: (message.chat.id in Config.CONVO_DICT)
|
|
||||||
and (not message.reactions)
|
|
||||||
)
|
|
@ -2,16 +2,20 @@ import asyncio
|
|||||||
import traceback
|
import traceback
|
||||||
from typing import Callable, Coroutine
|
from typing import Callable, Coroutine
|
||||||
|
|
||||||
from app import Config, bot
|
from pyrogram.enums import ChatType
|
||||||
from app.core import filters
|
|
||||||
from app.core.media_handler import MediaHandler
|
from app import BOT, Config, bot
|
||||||
from app.core.message import Message, Msg
|
from app.core.client import filters
|
||||||
|
from app.core.types.message import Message, Msg
|
||||||
|
from app.social_dl.media_handler import MediaHandler
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="dl")
|
@bot.add_cmd(cmd="dl")
|
||||||
async def dl(bot: bot, message: Message):
|
async def dl(bot: BOT, message: Message):
|
||||||
reply: Message = await bot.send_message(
|
reply: Message = await bot.send_message(
|
||||||
chat_id=message.chat.id, text="`trying to download...`"
|
chat_id=message.chat.id,
|
||||||
|
text="`trying to download...`",
|
||||||
|
disable_notification=message.chat.type == ChatType.CHANNEL,
|
||||||
)
|
)
|
||||||
coro: Coroutine = MediaHandler.process(message)
|
coro: Coroutine = MediaHandler.process(message)
|
||||||
task: asyncio.Task = asyncio.Task(coro, name=reply.task_id)
|
task: asyncio.Task = asyncio.Task(coro, name=reply.task_id)
|
||||||
@ -34,7 +38,7 @@ async def dl(bot: bot, message: Message):
|
|||||||
@bot.on_edited_message(filters.user_filter)
|
@bot.on_edited_message(filters.user_filter)
|
||||||
async def cmd_dispatcher(bot: bot, message: Msg):
|
async def cmd_dispatcher(bot: bot, message: Msg):
|
||||||
message: Message = Message.parse_message(message)
|
message: Message = Message.parse_message(message)
|
||||||
func: Callable = Config.CMD_DICT[message.cmd]
|
func: Callable = Config.CMD_DICT[message.cmd].func
|
||||||
coro: Coroutine = func(bot, message)
|
coro: Coroutine = func(bot, message)
|
||||||
try:
|
try:
|
||||||
task: asyncio.Task = asyncio.Task(coro, name=message.task_id)
|
task: asyncio.Task = asyncio.Task(coro, name=message.task_id)
|
||||||
@ -50,21 +54,6 @@ async def cmd_dispatcher(bot: bot, message: Msg):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bot.on_message(filters.convo_filter, group=0)
|
|
||||||
@bot.on_edited_message(filters.convo_filter, group=0)
|
|
||||||
async def convo_handler(bot: bot, message: Msg):
|
|
||||||
conv_dict: dict = Config.CONVO_DICT[message.chat.id]
|
|
||||||
conv_filters = conv_dict.get("filters")
|
|
||||||
if conv_filters:
|
|
||||||
check = await conv_filters(bot, message)
|
|
||||||
if not check:
|
|
||||||
message.continue_propagation()
|
|
||||||
conv_dict["response"] = message
|
|
||||||
message.continue_propagation()
|
|
||||||
conv_dict["response"] = message
|
|
||||||
message.continue_propagation()
|
|
||||||
|
|
||||||
|
|
||||||
@bot.on_message(filters.allowed_cmd_filter)
|
@bot.on_message(filters.allowed_cmd_filter)
|
||||||
@bot.on_message(filters.chat_filter)
|
@bot.on_message(filters.chat_filter)
|
||||||
async def dl_dispatcher(bot: bot, message: Msg):
|
async def dl_dispatcher(bot: bot, message: Msg):
|
||||||
@ -82,3 +71,14 @@ async def dl_dispatcher(bot: bot, message: Msg):
|
|||||||
func=dl.__name__,
|
func=dl.__name__,
|
||||||
name="traceback.txt",
|
name="traceback.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.on_message(filters.convo_filter, group=0)
|
||||||
|
@bot.on_edited_message(filters.convo_filter, group=0)
|
||||||
|
async def convo_handler(bot: BOT, message: Msg):
|
||||||
|
conv_obj: bot.Convo = bot.Convo.CONVO_DICT[message.chat.id]
|
||||||
|
if conv_obj.filters and not (await conv_obj.filters(bot, message)):
|
||||||
|
message.continue_propagation()
|
||||||
|
conv_obj.responses.append(message)
|
||||||
|
conv_obj.response.set_result(message)
|
||||||
|
message.continue_propagation()
|
@ -1,47 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
from pyrogram.filters import Filter
|
|
||||||
from pyrogram.types import Message
|
|
||||||
|
|
||||||
from app import Config
|
|
||||||
|
|
||||||
|
|
||||||
class Conversation:
|
|
||||||
class DuplicateConvo(Exception):
|
|
||||||
def __init__(self, chat: str | int | None = None):
|
|
||||||
text = "Conversation already started"
|
|
||||||
if chat:
|
|
||||||
text += f" with {chat}"
|
|
||||||
super().__init__(text)
|
|
||||||
|
|
||||||
class TimeOutError(Exception):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__("Conversation Timeout")
|
|
||||||
|
|
||||||
def __init__(self, chat_id: int, filters: Filter | None = None, timeout: int = 10):
|
|
||||||
self.chat_id = chat_id
|
|
||||||
self.filters = filters
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return json.dumps(self.__dict__, indent=4, ensure_ascii=False)
|
|
||||||
|
|
||||||
async def get_response(self, timeout: int | None = None) -> Message | None:
|
|
||||||
try:
|
|
||||||
async with asyncio.timeout(timeout or self.timeout):
|
|
||||||
while not Config.CONVO_DICT[self.chat_id]["response"]:
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
return Config.CONVO_DICT[self.chat_id]["response"]
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
raise self.TimeOutError
|
|
||||||
|
|
||||||
async def __aenter__(self) -> "Conversation":
|
|
||||||
if self.chat_id in Config.CONVO_DICT:
|
|
||||||
raise self.DuplicateConvo(self.chat_id)
|
|
||||||
convo_dict = {"filters": self.filters, "response": None}
|
|
||||||
Config.CONVO_DICT[self.chat_id] = convo_dict
|
|
||||||
return self
|
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
Config.CONVO_DICT.pop(self.chat_id, "")
|
|
26
app/core/logger.py
Normal file
26
app/core/logger.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
from logging import INFO, WARNING, StreamHandler, basicConfig, getLogger, handlers
|
||||||
|
|
||||||
|
os.makedirs("logs", exist_ok=True)
|
||||||
|
|
||||||
|
LOGGER = getLogger("Social-DL")
|
||||||
|
|
||||||
|
basicConfig(
|
||||||
|
level=INFO,
|
||||||
|
format="[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s",
|
||||||
|
datefmt="%y-%m-%d %H:%M:%S",
|
||||||
|
handlers={
|
||||||
|
handlers.RotatingFileHandler(
|
||||||
|
filename="logs/app_logs.txt",
|
||||||
|
mode="a",
|
||||||
|
maxBytes=5 * 1024 * 1024,
|
||||||
|
backupCount=2,
|
||||||
|
encoding=None,
|
||||||
|
delay=0,
|
||||||
|
),
|
||||||
|
StreamHandler(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
getLogger("pyrogram").setLevel(WARNING)
|
||||||
|
getLogger("httpx").setLevel(WARNING)
|
@ -1,244 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import traceback
|
|
||||||
from functools import lru_cache
|
|
||||||
from io import BytesIO
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from pyrogram.errors import MediaEmpty, PhotoSaveFileInvalid, WebpageCurlFailed
|
|
||||||
from pyrogram.types import InputMediaPhoto, InputMediaVideo
|
|
||||||
|
|
||||||
from app import Config, Message, bot
|
|
||||||
from app.api.gallery_dl import GalleryDL
|
|
||||||
from app.api.instagram import Instagram
|
|
||||||
from app.api.reddit import Reddit
|
|
||||||
from app.api.threads import Threads
|
|
||||||
from app.api.tiktok import Tiktok
|
|
||||||
from app.api.ytdl import YouTubeDL
|
|
||||||
from app.core import aiohttp_tools, shell
|
|
||||||
from app.core.scraper_config import MediaType
|
|
||||||
|
|
||||||
url_map: dict = {
|
|
||||||
"tiktok.com": Tiktok,
|
|
||||||
"www.instagram.com": Instagram,
|
|
||||||
"www.reddit.com": Reddit,
|
|
||||||
"reddit.com": Reddit,
|
|
||||||
"www.threads.net": Threads,
|
|
||||||
"twitter.com": GalleryDL,
|
|
||||||
"x.com": GalleryDL,
|
|
||||||
"www.x.com": GalleryDL,
|
|
||||||
"youtube.com": YouTubeDL,
|
|
||||||
"youtu.be": YouTubeDL,
|
|
||||||
"www.facebook.com": YouTubeDL,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache()
|
|
||||||
def get_url_dict_items():
|
|
||||||
return url_map.items()
|
|
||||||
|
|
||||||
|
|
||||||
class MediaHandler:
|
|
||||||
def __init__(self, message: Message) -> None:
|
|
||||||
self.exceptions = []
|
|
||||||
self.media_objects: list[asyncio.Task.result] = []
|
|
||||||
self.sender_dict = {}
|
|
||||||
self.__client: bot = message._client
|
|
||||||
self.message: Message = message
|
|
||||||
self.doc: bool = "-d" in message.flags
|
|
||||||
self.spoiler: bool = "-s" in message.flags
|
|
||||||
self.args_: dict = {
|
|
||||||
"chat_id": self.message.chat.id,
|
|
||||||
"reply_to_message_id": message.reply_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return json.dumps(self.__dict__, indent=4, ensure_ascii=False)
|
|
||||||
|
|
||||||
def get_sender(self, reply: bool = False) -> str:
|
|
||||||
if "-ns" in self.message.flags:
|
|
||||||
return ""
|
|
||||||
text: str = f"\nShared by : "
|
|
||||||
author: str | None = self.message.author_signature
|
|
||||||
sender: str = user.first_name if (user := self.message.from_user) else ""
|
|
||||||
reply_sender: str = ""
|
|
||||||
if reply:
|
|
||||||
reply_msg = self.message.replied
|
|
||||||
reply_sender: str = (
|
|
||||||
reply_user.first_name if (reply_user := reply_msg.from_user) else ""
|
|
||||||
)
|
|
||||||
if any((author, sender, reply_sender)):
|
|
||||||
return text + (author or sender if not reply else reply_sender)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
async def get_media(self) -> None:
|
|
||||||
async with asyncio.TaskGroup() as task_group:
|
|
||||||
tasks: list[asyncio.Task] = []
|
|
||||||
text_list: list[str] = self.message.text_list
|
|
||||||
reply_text_list: list[str] = self.message.reply_text_list
|
|
||||||
for link in text_list + reply_text_list:
|
|
||||||
reply: bool = link in reply_text_list
|
|
||||||
if match := url_map.get(urlparse(link).netloc):
|
|
||||||
tasks.append(task_group.create_task(match.start(link)))
|
|
||||||
self.sender_dict[link] = self.get_sender(reply=reply)
|
|
||||||
else:
|
|
||||||
for key, val in get_url_dict_items():
|
|
||||||
if key in link:
|
|
||||||
tasks.append(task_group.create_task(val.start(link)))
|
|
||||||
self.sender_dict[link] = self.get_sender(reply=reply)
|
|
||||||
self.media_objects: list[asyncio.Task.result] = [
|
|
||||||
task.result() for task in tasks if task.result()
|
|
||||||
]
|
|
||||||
|
|
||||||
async def send_media(self) -> None:
|
|
||||||
for obj in self.media_objects:
|
|
||||||
if "-nc" in self.message.flags:
|
|
||||||
caption = ""
|
|
||||||
else:
|
|
||||||
caption = (
|
|
||||||
obj.caption + obj.caption_url + self.sender_dict[obj.query_url]
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
if self.doc and not obj.in_dump:
|
|
||||||
await self.send_document(obj.media, caption=caption, path=obj.path)
|
|
||||||
continue
|
|
||||||
match obj.type:
|
|
||||||
case MediaType.MESSAGE:
|
|
||||||
await obj.media.copy(self.message.chat.id, caption=caption)
|
|
||||||
continue
|
|
||||||
case MediaType.GROUP:
|
|
||||||
await self.send_group(obj, caption=caption)
|
|
||||||
continue
|
|
||||||
case MediaType.PHOTO:
|
|
||||||
post = await self.send(
|
|
||||||
media={"photo": obj.media},
|
|
||||||
method=self.__client.send_photo,
|
|
||||||
caption=caption,
|
|
||||||
)
|
|
||||||
case MediaType.VIDEO:
|
|
||||||
post = await self.send(
|
|
||||||
media={"video": obj.media},
|
|
||||||
method=self.__client.send_video,
|
|
||||||
thumb=await aiohttp_tools.thumb_dl(obj.thumb),
|
|
||||||
caption=caption,
|
|
||||||
)
|
|
||||||
case MediaType.GIF:
|
|
||||||
post = await self.send(
|
|
||||||
media={"animation": obj.media},
|
|
||||||
method=self.__client.send_animation,
|
|
||||||
caption=caption,
|
|
||||||
unsave=True,
|
|
||||||
)
|
|
||||||
if obj.dump and Config.DUMP_ID:
|
|
||||||
await post.copy(Config.DUMP_ID, caption="#" + obj.shortcode)
|
|
||||||
except BaseException:
|
|
||||||
self.exceptions.append(
|
|
||||||
"\n".join([obj.caption_url.strip(), traceback.format_exc()])
|
|
||||||
)
|
|
||||||
|
|
||||||
async def send(self, media: dict | str, method, caption: str, **kwargs) -> Message:
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
post: Message = await method(
|
|
||||||
**media,
|
|
||||||
**self.args_,
|
|
||||||
caption=caption,
|
|
||||||
**kwargs,
|
|
||||||
has_spoiler=self.spoiler,
|
|
||||||
)
|
|
||||||
except (MediaEmpty, WebpageCurlFailed):
|
|
||||||
key, value = list(media.items())[0]
|
|
||||||
media[key] = await aiohttp_tools.in_memory_dl(value)
|
|
||||||
post: Message = await method(
|
|
||||||
**media,
|
|
||||||
**self.args_,
|
|
||||||
caption=caption,
|
|
||||||
**kwargs,
|
|
||||||
has_spoiler=self.spoiler,
|
|
||||||
)
|
|
||||||
except PhotoSaveFileInvalid:
|
|
||||||
post: Message = await self.__client.send_document(
|
|
||||||
**self.args_, document=media, caption=caption, force_document=True
|
|
||||||
)
|
|
||||||
return post
|
|
||||||
|
|
||||||
async def send_document(self, docs: list, caption: str, path="") -> None:
|
|
||||||
if not path:
|
|
||||||
docs = await asyncio.gather(
|
|
||||||
*[aiohttp_tools.in_memory_dl(doc) for doc in docs]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
[os.rename(file_, file_ + ".png") for file_ in glob.glob(f"{path}/*.webp")]
|
|
||||||
docs = glob.glob(f"{path}/*")
|
|
||||||
for doc in docs:
|
|
||||||
await self.__client.send_document(
|
|
||||||
**self.args_, document=doc, caption=caption, force_document=True
|
|
||||||
)
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
async def send_group(self, media_obj, caption: str) -> None:
|
|
||||||
sorted: list[str, list[InputMediaVideo | InputMediaPhoto]] = await sort_media(
|
|
||||||
caption=caption,
|
|
||||||
spoiler=self.spoiler,
|
|
||||||
urls=media_obj.media,
|
|
||||||
path=media_obj.path,
|
|
||||||
)
|
|
||||||
for data in sorted:
|
|
||||||
if isinstance(data, list):
|
|
||||||
await self.__client.send_media_group(**self.args_, media=data)
|
|
||||||
else:
|
|
||||||
await self.send(
|
|
||||||
media={"animation": data},
|
|
||||||
method=self.__client.send_animation,
|
|
||||||
caption=caption,
|
|
||||||
unsave=True,
|
|
||||||
)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def process(cls, message):
|
|
||||||
obj = cls(message=message)
|
|
||||||
await obj.get_media()
|
|
||||||
await obj.send_media()
|
|
||||||
[m_obj.cleanup() for m_obj in obj.media_objects]
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
async def sort_media(
|
|
||||||
caption="", spoiler=False, urls=None, path=None
|
|
||||||
) -> list[str, list[InputMediaVideo | InputMediaPhoto]]:
|
|
||||||
images, videos, animations = [], [], []
|
|
||||||
if path and os.path.exists(path):
|
|
||||||
[os.rename(file_, file_ + ".png") for file_ in glob.glob(f"{path}/*.webp")]
|
|
||||||
media: list[str] = glob.glob(f"{path}/*")
|
|
||||||
else:
|
|
||||||
media: tuple[BytesIO] = await asyncio.gather(
|
|
||||||
*[aiohttp_tools.in_memory_dl(url) for url in urls]
|
|
||||||
)
|
|
||||||
for file in media:
|
|
||||||
if path:
|
|
||||||
name: str = file.lower()
|
|
||||||
else:
|
|
||||||
name: str = file.name.lower()
|
|
||||||
if name.endswith((".png", ".jpg", ".jpeg")):
|
|
||||||
images.append(InputMediaPhoto(file, caption=caption, has_spoiler=spoiler))
|
|
||||||
elif name.endswith((".mp4", ".mkv", ".webm")):
|
|
||||||
if not urls and not await shell.check_audio(file):
|
|
||||||
animations.append(file)
|
|
||||||
else:
|
|
||||||
videos.append(
|
|
||||||
InputMediaVideo(file, caption=caption, has_spoiler=spoiler)
|
|
||||||
)
|
|
||||||
elif name.endswith(".gif"):
|
|
||||||
animations.append(file)
|
|
||||||
return await make_chunks(images, videos, animations)
|
|
||||||
|
|
||||||
|
|
||||||
async def make_chunks(
|
|
||||||
images=[], videos=[], animations=[]
|
|
||||||
) -> list[str, list[InputMediaVideo | InputMediaPhoto]]:
|
|
||||||
chunk_imgs = [images[imgs : imgs + 5] for imgs in range(0, len(images), 5)]
|
|
||||||
chunk_vids = [videos[vids : vids + 5] for vids in range(0, len(videos), 5)]
|
|
||||||
return [*chunk_imgs, *chunk_vids, *animations]
|
|
@ -1,44 +0,0 @@
|
|||||||
import json
|
|
||||||
import shutil
|
|
||||||
from enum import Enum, auto
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
|
|
||||||
class MediaType(Enum):
|
|
||||||
PHOTO = auto()
|
|
||||||
VIDEO = auto()
|
|
||||||
GROUP = auto()
|
|
||||||
GIF = auto()
|
|
||||||
MESSAGE = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class ScraperConfig:
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.dump: bool = False
|
|
||||||
self.in_dump: bool = False
|
|
||||||
self.path: str = ""
|
|
||||||
self.media: str | BytesIO = ""
|
|
||||||
self.caption: str = ""
|
|
||||||
self.caption_url: str = ""
|
|
||||||
self.thumb: str | None | BytesIO = None
|
|
||||||
self.type: None | MediaType = None
|
|
||||||
self.success: bool = False
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return json.dumps(self.__dict__, indent=4, ensure_ascii=False, default=str)
|
|
||||||
|
|
||||||
def set_sauce(self, url: str) -> None:
|
|
||||||
self.caption_url = f"\n\n<a href='{url}'>Sauce</a>"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def start(cls, url: str) -> "ScraperConfig":
|
|
||||||
obj = cls(url=url)
|
|
||||||
obj.query_url = url
|
|
||||||
obj.set_sauce(url)
|
|
||||||
await obj.download_or_extract()
|
|
||||||
if obj.success:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
|
||||||
if self.path:
|
|
||||||
shutil.rmtree(self.path, ignore_errors=True)
|
|
@ -7,20 +7,28 @@ from pyrogram.types import Message as Msg
|
|||||||
from pyrogram.types import User
|
from pyrogram.types import User
|
||||||
|
|
||||||
from app import Config
|
from app import Config
|
||||||
from app.core.conversation import Conversation
|
|
||||||
|
|
||||||
|
|
||||||
class Message(Msg):
|
class Message(Msg):
|
||||||
def __init__(self, message: Msg) -> None:
|
def __init__(self, message: Msg) -> None:
|
||||||
args = vars(message)
|
args = vars(message).copy()
|
||||||
args["client"] = args.pop("_client", None)
|
args["client"] = args.pop("_client", message._client)
|
||||||
super().__init__(**args)
|
super().__init__(**args)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def cmd(self) -> str | None:
|
def cmd(self) -> str | None:
|
||||||
|
if not self.text_list:
|
||||||
|
return
|
||||||
raw_cmd = self.text_list[0]
|
raw_cmd = self.text_list[0]
|
||||||
cmd = raw_cmd[1:]
|
cmd = raw_cmd[1:]
|
||||||
return cmd if cmd in Config.CMD_DICT else None
|
return (
|
||||||
|
cmd
|
||||||
|
if (
|
||||||
|
cmd in Config.CMD_DICT.keys()
|
||||||
|
or cmd in {"dl", "down", "download", "play", "song"}
|
||||||
|
)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def flags(self) -> list:
|
def flags(self) -> list:
|
||||||
@ -40,6 +48,12 @@ class Message(Msg):
|
|||||||
return self.text.split(maxsplit=1)[-1]
|
return self.text.split(maxsplit=1)[-1]
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_from_owner(self) -> bool:
|
||||||
|
if self.from_user and self.from_user.id == Config.OWNER_ID:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def replied(self) -> "Message":
|
def replied(self) -> "Message":
|
||||||
if self.reply_to_message:
|
if self.reply_to_message:
|
||||||
@ -55,10 +69,7 @@ class Message(Msg):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def reply_text_list(self) -> list:
|
def reply_text_list(self) -> list:
|
||||||
reply_text_list = []
|
return self.replied.text_list if self.replied else []
|
||||||
if self.replied and self.replied.text and "dl" in self.text_list[0]:
|
|
||||||
reply_text_list = self.replied.text_list
|
|
||||||
return reply_text_list
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def task_id(self):
|
def task_id(self):
|
||||||
@ -66,7 +77,13 @@ class Message(Msg):
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def text_list(self) -> list:
|
def text_list(self) -> list:
|
||||||
|
if self.text:
|
||||||
return self.text.split()
|
return self.text.split()
|
||||||
|
return []
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def trigger(self):
|
||||||
|
return Config.CMD_TRIGGER
|
||||||
|
|
||||||
async def async_deleter(self, del_in, task, block) -> None:
|
async def async_deleter(self, del_in, task, block) -> None:
|
||||||
if block:
|
if block:
|
||||||
@ -124,12 +141,12 @@ class Message(Msg):
|
|||||||
|
|
||||||
async def get_response(self, filters: Filter = None, timeout: int = 8):
|
async def get_response(self, filters: Filter = None, timeout: int = 8):
|
||||||
try:
|
try:
|
||||||
async with Conversation(
|
async with self._client.Convo(
|
||||||
chat_id=self.chat.id, filters=filters, timeout=timeout
|
chat_id=self.chat.id, filters=filters, timeout=timeout
|
||||||
) as convo:
|
) as convo:
|
||||||
response: Message | None = await convo.get_response()
|
response: Message | None = await convo.get_response()
|
||||||
return response
|
return response
|
||||||
except Conversation.TimeOutError:
|
except TimeoutError:
|
||||||
return
|
return
|
||||||
|
|
||||||
async def reply(
|
async def reply(
|
46
app/plugins/admin/ban.py
Normal file
46
app/plugins/admin/ban.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import asyncio
|
||||||
|
from typing import Awaitable
|
||||||
|
|
||||||
|
from pyrogram.types import User
|
||||||
|
|
||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd=["ban", "unban"])
|
||||||
|
async def ban_or_unban(bot: BOT, message: Message) -> None:
|
||||||
|
user, reason = await message.extract_user_n_reason()
|
||||||
|
if not isinstance(user, User):
|
||||||
|
await message.reply(user, del_in=10)
|
||||||
|
return
|
||||||
|
if message.cmd == "ban":
|
||||||
|
action: Awaitable = bot.ban_chat_member(
|
||||||
|
chat_id=message.chat.id, user_id=user.id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
action: Awaitable = bot.unban_chat_member(
|
||||||
|
chat_id=message.chat.id, user_id=user.id
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await action
|
||||||
|
await message.reply(
|
||||||
|
text=f"{message.cmd.capitalize()}ned: {user.mention}\nReason: {reason}."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(text=e, del_in=10)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="kick")
|
||||||
|
async def kick_user(bot: BOT, message: Message):
|
||||||
|
user, reason = await message.extract_user_n_reason()
|
||||||
|
if not isinstance(user, User):
|
||||||
|
await message.reply(user, del_in=10)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await bot.ban_chat_member(chat_id=message.chat.id, user_id=user.id)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await bot.unban_chat_member(chat_id=message.chat.id, user_id=user.id)
|
||||||
|
await message.reply(
|
||||||
|
text=f"{message.cmd.capitalize()}ed: {user.mention}\nReason: {reason}."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(text=e, del_in=10)
|
32
app/plugins/admin/mute.py
Normal file
32
app/plugins/admin/mute.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from pyrogram.types import ChatPermissions, User
|
||||||
|
|
||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd=["mute", "unmute"])
|
||||||
|
async def mute_or_unmute(bot: BOT, message: Message):
|
||||||
|
user, reason = await message.extract_user_n_reason()
|
||||||
|
if not isinstance(user, User):
|
||||||
|
await message.reply(user, del_in=10)
|
||||||
|
return
|
||||||
|
perms = message.chat.permissions
|
||||||
|
if message.cmd == "mute":
|
||||||
|
perms = ChatPermissions(
|
||||||
|
can_send_messages=False,
|
||||||
|
can_pin_messages=False,
|
||||||
|
can_invite_users=False,
|
||||||
|
can_change_info=False,
|
||||||
|
can_send_media_messages=False,
|
||||||
|
can_send_polls=False,
|
||||||
|
can_send_other_messages=False,
|
||||||
|
can_add_web_page_previews=False,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await bot.restrict_chat_member(
|
||||||
|
chat_id=message.chat.id, user_id=user.id, permissions=perms
|
||||||
|
)
|
||||||
|
await message.reply(
|
||||||
|
text=f"{message.cmd.capitalize()}d: {user.mention}\nReason: {reason}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(text=e, del_in=10)
|
82
app/plugins/admin/promote.py
Normal file
82
app/plugins/admin/promote.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from pyrogram.types import ChatPrivileges, User
|
||||||
|
|
||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
def get_privileges(
|
||||||
|
anon: bool = False, full: bool = False, without_rights: bool = False
|
||||||
|
) -> ChatPrivileges:
|
||||||
|
if without_rights:
|
||||||
|
return ChatPrivileges(
|
||||||
|
can_manage_chat=True,
|
||||||
|
can_manage_video_chats=False,
|
||||||
|
can_pin_messages=False,
|
||||||
|
can_delete_messages=False,
|
||||||
|
can_change_info=False,
|
||||||
|
can_restrict_members=False,
|
||||||
|
can_invite_users=False,
|
||||||
|
can_promote_members=False,
|
||||||
|
is_anonymous=False,
|
||||||
|
)
|
||||||
|
return ChatPrivileges(
|
||||||
|
can_manage_chat=True,
|
||||||
|
can_manage_video_chats=True,
|
||||||
|
can_pin_messages=True,
|
||||||
|
can_delete_messages=True,
|
||||||
|
can_change_info=True,
|
||||||
|
can_restrict_members=True,
|
||||||
|
can_invite_users=True,
|
||||||
|
can_promote_members=full,
|
||||||
|
is_anonymous=anon,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd=["promote", "demote"])
|
||||||
|
async def promote_or_demote(bot: BOT, message: Message) -> None:
|
||||||
|
"""
|
||||||
|
CMD: PROMOTE | DEMOTE
|
||||||
|
INFO: Add/Remove an Admin.
|
||||||
|
FLAGS:
|
||||||
|
PROMOTE: -full for full rights, -anon for anon admin
|
||||||
|
USAGE:
|
||||||
|
PROMOTE: .promote [ -anon | -full ] [ UID | REPLY | @ ] Title[Optional]
|
||||||
|
DEMOTE: .demote [ UID | REPLY | @ ]
|
||||||
|
"""
|
||||||
|
response: Message = await message.reply(
|
||||||
|
f"Trying to {message.cmd.capitalize()}....."
|
||||||
|
)
|
||||||
|
user, title = await message.extract_user_n_reason()
|
||||||
|
if not isinstance(user, User):
|
||||||
|
await response.edit(user, del_in=10)
|
||||||
|
return
|
||||||
|
full: bool = "-full" in message.flags
|
||||||
|
anon: bool = "-anon" in message.flags
|
||||||
|
without_rights = "-wr" in message.flags
|
||||||
|
promote = message.cmd == "promote"
|
||||||
|
if promote:
|
||||||
|
privileges: ChatPrivileges = get_privileges(
|
||||||
|
full=full, anon=anon, without_rights=without_rights
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
privileges = ChatPrivileges(can_manage_chat=False)
|
||||||
|
response_text = f"{message.cmd.capitalize()}d: {user.mention}"
|
||||||
|
try:
|
||||||
|
await bot.promote_chat_member(
|
||||||
|
chat_id=message.chat.id, user_id=user.id, privileges=privileges
|
||||||
|
)
|
||||||
|
if promote:
|
||||||
|
# Let server promote admin before setting title
|
||||||
|
# Bot is too fast moment 😂😂😂
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
await bot.set_administrator_title(
|
||||||
|
chat_id=message.chat.id, user_id=user.id, title=title or "Admin"
|
||||||
|
)
|
||||||
|
if title:
|
||||||
|
response_text += f"\nTitle: {title}"
|
||||||
|
if without_rights:
|
||||||
|
response_text += "\nWithout Rights: True"
|
||||||
|
await response.edit(text=response_text)
|
||||||
|
except Exception as e:
|
||||||
|
await response.edit(text=e, del_in=10, block=True)
|
37
app/plugins/admin/zombies.py
Normal file
37
app/plugins/admin/zombies.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from pyrogram.enums import ChatMemberStatus
|
||||||
|
from pyrogram.errors import FloodWait
|
||||||
|
|
||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="zombies")
|
||||||
|
async def clean_zombies(bot: BOT, message: Message):
|
||||||
|
me = await bot.get_chat_member(message.chat.id, bot.me.id)
|
||||||
|
if me.status not in {ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER}:
|
||||||
|
await message.reply("Cannot clean zombies without being admin.")
|
||||||
|
return
|
||||||
|
zombies = 0
|
||||||
|
admin_zombies = 0
|
||||||
|
response = await message.reply("Cleaning Zombies....\nthis may take a while")
|
||||||
|
async for member in bot.get_chat_members(message.chat.id):
|
||||||
|
try:
|
||||||
|
if member.user.is_deleted:
|
||||||
|
if member.status in {
|
||||||
|
ChatMemberStatus.ADMINISTRATOR,
|
||||||
|
ChatMemberStatus.OWNER,
|
||||||
|
}:
|
||||||
|
admin_zombies += 1
|
||||||
|
continue
|
||||||
|
zombies += 1
|
||||||
|
await bot.ban_chat_member(
|
||||||
|
chat_id=message.chat.id, user_id=member.user.id
|
||||||
|
)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except FloodWait as e:
|
||||||
|
await asyncio.sleep(e.value + 3)
|
||||||
|
resp_str = f"Cleaned <b>{zombies}</b> zombies."
|
||||||
|
if admin_zombies:
|
||||||
|
resp_str += f"\n<b>{admin_zombies}</b> Admin Zombie(s) not Removed."
|
||||||
|
await response.edit(resp_str)
|
@ -1,144 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from typing import Awaitable
|
|
||||||
|
|
||||||
from pyrogram.types import ChatPermissions, ChatPrivileges, User
|
|
||||||
|
|
||||||
from app import bot
|
|
||||||
from app.core.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
def get_privileges(
|
|
||||||
anon: bool = False, full: bool = False, without_rights: bool = False
|
|
||||||
) -> ChatPrivileges:
|
|
||||||
if without_rights:
|
|
||||||
return ChatPrivileges(
|
|
||||||
can_manage_chat=True,
|
|
||||||
can_manage_video_chats=False,
|
|
||||||
can_pin_messages=False,
|
|
||||||
can_delete_messages=False,
|
|
||||||
can_change_info=False,
|
|
||||||
can_restrict_members=False,
|
|
||||||
can_invite_users=False,
|
|
||||||
can_promote_members=False,
|
|
||||||
is_anonymous=False,
|
|
||||||
)
|
|
||||||
return ChatPrivileges(
|
|
||||||
can_manage_chat=True,
|
|
||||||
can_manage_video_chats=True,
|
|
||||||
can_pin_messages=True,
|
|
||||||
can_delete_messages=True,
|
|
||||||
can_change_info=True,
|
|
||||||
can_restrict_members=True,
|
|
||||||
can_invite_users=True,
|
|
||||||
can_promote_members=full,
|
|
||||||
is_anonymous=anon,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd=["promote", "demote"])
|
|
||||||
async def promote_or_demote(bot: bot, message: Message) -> None:
|
|
||||||
response: Message = await message.reply(
|
|
||||||
f"Trying to {message.cmd.capitalize()}....."
|
|
||||||
)
|
|
||||||
user, title = await message.extract_user_n_reason()
|
|
||||||
if not isinstance(user, User):
|
|
||||||
await response.edit(user, del_in=10)
|
|
||||||
return
|
|
||||||
full: bool = "-full" in message.flags
|
|
||||||
anon: bool = "-anon" in message.flags
|
|
||||||
without_rights = "-wr" in message.flags
|
|
||||||
promote = message.cmd == "promote"
|
|
||||||
if promote:
|
|
||||||
privileges: ChatPrivileges = get_privileges(
|
|
||||||
full=full, anon=anon, without_rights=without_rights
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
privileges = ChatPrivileges(can_manage_chat=False)
|
|
||||||
response_text = f"{message.cmd.capitalize()}d: {user.mention}"
|
|
||||||
try:
|
|
||||||
await bot.promote_chat_member(
|
|
||||||
chat_id=message.chat.id, user_id=user.id, privileges=privileges
|
|
||||||
)
|
|
||||||
if promote:
|
|
||||||
# Let server promote admin before setting title
|
|
||||||
# Bot is too fast moment 😂😂😂
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
await bot.set_administrator_title(
|
|
||||||
chat_id=message.chat.id, user_id=user.id, title=title or "Admin"
|
|
||||||
)
|
|
||||||
if title:
|
|
||||||
response_text += f"\nTitle: {title}"
|
|
||||||
if without_rights:
|
|
||||||
response_text += "\nWithout Rights: True"
|
|
||||||
await response.edit(text=response_text)
|
|
||||||
except Exception as e:
|
|
||||||
await response.edit(text=e, del_in=10, block=True)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd=["ban", "unban"])
|
|
||||||
async def ban_or_unban(bot: bot, message: Message) -> None:
|
|
||||||
user, reason = await message.extract_user_n_reason()
|
|
||||||
if not isinstance(user, User):
|
|
||||||
await message.reply(user, del_in=10)
|
|
||||||
return
|
|
||||||
if message.cmd == "ban":
|
|
||||||
action: Awaitable = bot.ban_chat_member(
|
|
||||||
chat_id=message.chat.id, user_id=user.id
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
action: Awaitable = bot.unban_chat_member(
|
|
||||||
chat_id=message.chat.id, user_id=user.id
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await action
|
|
||||||
await message.reply(
|
|
||||||
text=f"{message.cmd.capitalize()}ned: {user.mention}\nReason: {reason}."
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
await message.reply(text=e, del_in=10)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="kick")
|
|
||||||
async def kick_user(bot, message: Message):
|
|
||||||
user, reason = await message.extract_user_n_reason()
|
|
||||||
if not isinstance(user, User):
|
|
||||||
await message.reply(user, del_in=10)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await bot.ban_chat_member(chat_id=message.chat.id, user_id=user.id)
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
await bot.unban_chat_member(chat_id=message.chat.id, user_id=user.id)
|
|
||||||
await message.reply(
|
|
||||||
text=f"{message.cmd.capitalize()}ed: {user.mention}\nReason: {reason}."
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
await message.reply(text=e, del_in=10)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd=["mute", "unmute"])
|
|
||||||
async def mute_or_unmute(bot: bot, message: Message):
|
|
||||||
user, reason = await message.extract_user_n_reason()
|
|
||||||
if not isinstance(user, User):
|
|
||||||
await message.reply(user, del_in=10)
|
|
||||||
return
|
|
||||||
perms = message.chat.permissions
|
|
||||||
if message.cmd == "mute":
|
|
||||||
perms = ChatPermissions(
|
|
||||||
can_send_messages=False,
|
|
||||||
can_pin_messages=False,
|
|
||||||
can_invite_users=False,
|
|
||||||
can_change_info=False,
|
|
||||||
can_send_media_messages=False,
|
|
||||||
can_send_polls=False,
|
|
||||||
can_send_other_messages=False,
|
|
||||||
can_add_web_page_previews=False,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await bot.restrict_chat_member(
|
|
||||||
chat_id=message.chat.id, user_id=user.id, permissions=perms
|
|
||||||
)
|
|
||||||
await message.reply(
|
|
||||||
text=f"{message.cmd.capitalize()}d: {user.mention}\nReason: {reason}."
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
await message.reply(text=e, del_in=10)
|
|
@ -7,8 +7,8 @@ from urllib.parse import urlparse
|
|||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
|
||||||
from app import Message, bot
|
from app import Message, bot
|
||||||
from app.api.ytdl import FakeLogger
|
from app.social_dl.api.ytdl import FakeLogger
|
||||||
from app.core.aiohttp_tools import in_memory_dl
|
from app.utils.aiohttp_tools import in_memory_dl
|
||||||
|
|
||||||
domains = [
|
domains = [
|
||||||
"www.youtube.com",
|
"www.youtube.com",
|
||||||
@ -23,8 +23,7 @@ domains = [
|
|||||||
@bot.add_cmd(cmd="song")
|
@bot.add_cmd(cmd="song")
|
||||||
async def song_dl(bot: bot, message: Message) -> None | Message:
|
async def song_dl(bot: bot, message: Message) -> None | Message:
|
||||||
reply_query = None
|
reply_query = None
|
||||||
if message.replied:
|
for link in message.reply_text_list:
|
||||||
for link in message.replied.text_list:
|
|
||||||
if urlparse(link).netloc in domains:
|
if urlparse(link).netloc in domains:
|
||||||
reply_query = link
|
reply_query = link
|
||||||
break
|
break
|
61
app/plugins/dev/exec.py
Normal file
61
app/plugins/dev/exec.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from pyrogram.enums import ParseMode
|
||||||
|
|
||||||
|
from app import Config, bot, BOT, Message # isort:skip
|
||||||
|
from app.utils import shell, aiohttp_tools as aio # isort:skip
|
||||||
|
|
||||||
|
|
||||||
|
async def executor(bot: BOT, message: Message) -> Message | None:
|
||||||
|
"""
|
||||||
|
CMD: EXEC
|
||||||
|
INFO: Run Python Code.
|
||||||
|
FLAGS: -s to only show output.
|
||||||
|
USAGE:
|
||||||
|
.exec [-s] return 1
|
||||||
|
"""
|
||||||
|
code: str = message.flt_input.strip()
|
||||||
|
if not code:
|
||||||
|
return await message.reply("exec Jo mama?")
|
||||||
|
reply: Message = await message.reply("executing")
|
||||||
|
sys.stdout = codeOut = StringIO()
|
||||||
|
sys.stderr = codeErr = StringIO()
|
||||||
|
# Indent code as per proper python syntax
|
||||||
|
formatted_code = "\n ".join(code.splitlines())
|
||||||
|
try:
|
||||||
|
# Create and initialise the function
|
||||||
|
exec(f"async def _exec(bot, message):\n {formatted_code}")
|
||||||
|
task = asyncio.Task(locals()["_exec"](bot, message), name=reply.task_id)
|
||||||
|
func_out = await task
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
return await reply.edit("`Cancelled....`")
|
||||||
|
except BaseException:
|
||||||
|
func_out = str(traceback.format_exc())
|
||||||
|
sys.stdout = sys.__stdout__
|
||||||
|
sys.stderr = sys.__stderr__
|
||||||
|
output = codeErr.getvalue().strip() or codeOut.getvalue().strip()
|
||||||
|
if func_out is not None:
|
||||||
|
output = f"{output}\n\n{func_out}".strip()
|
||||||
|
elif not output and "-s" in message.flags:
|
||||||
|
await reply.delete()
|
||||||
|
return
|
||||||
|
if "-s" in message.flags:
|
||||||
|
output = f">> `{output}`"
|
||||||
|
else:
|
||||||
|
output = f"```python\n> {code}```\n\n>> `{output}`"
|
||||||
|
await reply.edit(
|
||||||
|
output,
|
||||||
|
name="exec.txt",
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
parse_mode=ParseMode.MARKDOWN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if Config.DEV_MODE:
|
||||||
|
Config.CMD_DICT["py"] = Config.CMD(
|
||||||
|
func=executor, path=inspect.stack()[0][1], doc=executor.__doc__
|
||||||
|
)
|
31
app/plugins/dev/loader.py
Normal file
31
app/plugins/dev/loader.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from app import BOT, Config, Message
|
||||||
|
|
||||||
|
|
||||||
|
async def loader(bot: BOT, message: Message) -> Message | None:
|
||||||
|
if (
|
||||||
|
not message.replied
|
||||||
|
or not message.replied.document
|
||||||
|
or not message.replied.document.file_name.endswith(".py")
|
||||||
|
):
|
||||||
|
return await message.reply("reply to a plugin.")
|
||||||
|
reply: Message = await message.reply("Loading....")
|
||||||
|
file_name: str = message.replied.document.file_name.rstrip(".py")
|
||||||
|
reload = sys.modules.pop(f"app.temp.{file_name}", None)
|
||||||
|
status: str = "Reloaded" if reload else "Loaded"
|
||||||
|
await message.replied.download("app/temp/")
|
||||||
|
try:
|
||||||
|
importlib.import_module(f"app.temp.{file_name}")
|
||||||
|
except BaseException:
|
||||||
|
return await reply.edit(str(traceback.format_exc()))
|
||||||
|
await reply.edit(f"{status} {file_name}.py.")
|
||||||
|
|
||||||
|
|
||||||
|
if Config.DEV_MODE:
|
||||||
|
Config.CMD_DICT["load"] = Config.CMD(
|
||||||
|
func=loader, path=inspect.stack()[0][1], doc=loader.__doc__
|
||||||
|
)
|
60
app/plugins/dev/shell.py
Normal file
60
app/plugins/dev/shell.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import asyncio
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
from pyrogram.enums import ParseMode
|
||||||
|
|
||||||
|
from app import BOT, Config, Message
|
||||||
|
from app.utils import shell
|
||||||
|
|
||||||
|
|
||||||
|
async def run_cmd(bot: BOT, message: Message) -> Message | None:
|
||||||
|
cmd: str = message.input.strip()
|
||||||
|
reply: Message = await message.reply("executing...")
|
||||||
|
try:
|
||||||
|
proc_stdout: str = await asyncio.Task(
|
||||||
|
shell.run_shell_cmd(cmd), name=reply.task_id
|
||||||
|
)
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
return await reply.edit("`Cancelled...`")
|
||||||
|
output: str = f"<pre language=shell>~${cmd}\n\n{proc_stdout}</pre>"
|
||||||
|
return await reply.edit(output, name="sh.txt", disable_web_page_preview=True)
|
||||||
|
|
||||||
|
|
||||||
|
# Shell with Live Output
|
||||||
|
async def live_shell(bot: BOT, message: Message) -> Message | None:
|
||||||
|
cmd: str = message.input.strip()
|
||||||
|
reply: Message = await message.reply("`getting live output....`")
|
||||||
|
sub_process: shell.AsyncShell = await shell.AsyncShell.run_cmd(cmd)
|
||||||
|
sleep_for: int = 1
|
||||||
|
output: str = ""
|
||||||
|
try:
|
||||||
|
async for stdout in sub_process.get_output():
|
||||||
|
if output != stdout:
|
||||||
|
if len(stdout) <= 4096:
|
||||||
|
await reply.edit(
|
||||||
|
f"```shell\n{stdout}```",
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
parse_mode=ParseMode.MARKDOWN,
|
||||||
|
)
|
||||||
|
output = stdout
|
||||||
|
if sleep_for >= 6:
|
||||||
|
sleep_for = 1
|
||||||
|
await asyncio.Task(asyncio.sleep(sleep_for), name=reply.task_id)
|
||||||
|
sleep_for += 1
|
||||||
|
return await reply.edit(
|
||||||
|
f"<pre language=shell>~${cmd}\n\n{sub_process.full_std}</pre>",
|
||||||
|
name="shell.txt",
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
sub_process.cancel()
|
||||||
|
return await reply.edit(f"`Cancelled....`")
|
||||||
|
|
||||||
|
|
||||||
|
if Config.DEV_MODE:
|
||||||
|
Config.CMD_DICT["shell"] = Config.CMD(
|
||||||
|
func=live_shell, path=inspect.stack()[0][1], doc=live_shell.__doc__
|
||||||
|
)
|
||||||
|
Config.CMD_DICT["sh"] = Config.CMD(
|
||||||
|
func=run_cmd, path=inspect.stack()[0][1], doc=run_cmd.__doc__
|
||||||
|
)
|
@ -1,143 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import importlib
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from pyrogram.enums import ParseMode
|
|
||||||
|
|
||||||
from app import Config, Message, bot
|
|
||||||
from app.core import shell, aiohttp_tools as aio # isort:skip
|
|
||||||
|
|
||||||
|
|
||||||
# Run shell commands
|
|
||||||
async def run_cmd(bot: bot, message: Message) -> Message | None:
|
|
||||||
cmd: str = message.input.strip()
|
|
||||||
reply: Message = await message.reply("executing...")
|
|
||||||
try:
|
|
||||||
proc_stdout: str = await asyncio.Task(
|
|
||||||
shell.run_shell_cmd(cmd), name=reply.task_id
|
|
||||||
)
|
|
||||||
except asyncio.exceptions.CancelledError:
|
|
||||||
return await reply.edit("`Cancelled...`")
|
|
||||||
output: str = f"~$`{cmd}`\n\n`{proc_stdout}`"
|
|
||||||
return await reply.edit(output, name="sh.txt", disable_web_page_preview=True)
|
|
||||||
|
|
||||||
|
|
||||||
# Shell with Live Output
|
|
||||||
async def live_shell(bot: bot, message: Message) -> Message | None:
|
|
||||||
cmd: str = message.input.strip()
|
|
||||||
reply: Message = await message.reply("`getting live output....`")
|
|
||||||
sub_process: shell.AsyncShell = await shell.AsyncShell.run_cmd(cmd)
|
|
||||||
sleep_for: int = 1
|
|
||||||
output: str = ""
|
|
||||||
try:
|
|
||||||
async for stdout in sub_process.get_output():
|
|
||||||
if output != stdout:
|
|
||||||
if len(stdout) <= 4096:
|
|
||||||
await reply.edit(
|
|
||||||
f"`{stdout}`",
|
|
||||||
disable_web_page_preview=True,
|
|
||||||
parse_mode=ParseMode.MARKDOWN,
|
|
||||||
)
|
|
||||||
output = stdout
|
|
||||||
if sleep_for >= 6:
|
|
||||||
sleep_for = 1
|
|
||||||
await asyncio.Task(asyncio.sleep(sleep_for), name=reply.task_id)
|
|
||||||
sleep_for += 1
|
|
||||||
return await reply.edit(
|
|
||||||
f"~$`{cmd}\n\n``{sub_process.full_std}`",
|
|
||||||
name="shell.txt",
|
|
||||||
disable_web_page_preview=True,
|
|
||||||
)
|
|
||||||
except asyncio.exceptions.CancelledError:
|
|
||||||
sub_process.cancel()
|
|
||||||
return await reply.edit(f"`Cancelled....`")
|
|
||||||
|
|
||||||
|
|
||||||
# Run Python code
|
|
||||||
|
|
||||||
|
|
||||||
async def executor(bot: bot, message: Message) -> Message | None:
|
|
||||||
code: str = message.flt_input.strip()
|
|
||||||
if not code:
|
|
||||||
return await message.reply("exec Jo mama?")
|
|
||||||
reply: Message = await message.reply("executing")
|
|
||||||
sys.stdout = codeOut = StringIO()
|
|
||||||
sys.stderr = codeErr = StringIO()
|
|
||||||
# Indent code as per proper python syntax
|
|
||||||
formatted_code = "\n ".join(code.splitlines())
|
|
||||||
try:
|
|
||||||
# Create and initialise the function
|
|
||||||
exec(f"async def _exec(bot, message):\n {formatted_code}")
|
|
||||||
func_out = await asyncio.Task(
|
|
||||||
locals()["_exec"](bot, message), name=reply.task_id
|
|
||||||
)
|
|
||||||
except asyncio.exceptions.CancelledError:
|
|
||||||
return await reply.edit("`Cancelled....`")
|
|
||||||
except BaseException:
|
|
||||||
func_out = str(traceback.format_exc())
|
|
||||||
sys.stdout = sys.__stdout__
|
|
||||||
sys.stderr = sys.__stderr__
|
|
||||||
output = codeErr.getvalue().strip() or codeOut.getvalue().strip()
|
|
||||||
if func_out is not None:
|
|
||||||
output = f"{output}\n\n{func_out}".strip()
|
|
||||||
elif not output and "-s" in message.flags:
|
|
||||||
await reply.delete()
|
|
||||||
return
|
|
||||||
if "-s" in message.flags:
|
|
||||||
output = f">> `{output}`"
|
|
||||||
else:
|
|
||||||
output = f"> `{code}`\n\n>> `{output}`"
|
|
||||||
await reply.edit(
|
|
||||||
output,
|
|
||||||
name="exec.txt",
|
|
||||||
disable_web_page_preview=True,
|
|
||||||
parse_mode=ParseMode.MARKDOWN,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def loader(bot: bot, message: Message) -> Message | None:
|
|
||||||
if (
|
|
||||||
not message.replied
|
|
||||||
or not message.replied.document
|
|
||||||
or not message.replied.document.file_name.endswith(".py")
|
|
||||||
):
|
|
||||||
return await message.reply("reply to a plugin.")
|
|
||||||
reply: Message = await message.reply("Loading....")
|
|
||||||
file_name: str = message.replied.document.file_name.rstrip(".py")
|
|
||||||
reload = sys.modules.pop(f"app.temp.{file_name}", None)
|
|
||||||
status: str = "Reloaded" if reload else "Loaded"
|
|
||||||
await message.replied.download("app/temp/")
|
|
||||||
try:
|
|
||||||
importlib.import_module(f"app.temp.{file_name}")
|
|
||||||
except BaseException:
|
|
||||||
return await reply.edit(str(traceback.format_exc()))
|
|
||||||
await reply.edit(f"{status} {file_name}.py.")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="c")
|
|
||||||
async def cancel_task(bot: bot, message: Message) -> Message | None:
|
|
||||||
task_id: str | None = message.replied_task_id
|
|
||||||
if not task_id:
|
|
||||||
return await message.reply(
|
|
||||||
text="Reply To a Command or Bot's Response Message.", del_in=8
|
|
||||||
)
|
|
||||||
all_tasks: set[asyncio.all_tasks] = asyncio.all_tasks()
|
|
||||||
tasks: list[asyncio.Task] | None = [x for x in all_tasks if x.get_name() == task_id]
|
|
||||||
if not tasks:
|
|
||||||
return await message.reply(
|
|
||||||
text="Task not in Currently Running Tasks.", del_in=8
|
|
||||||
)
|
|
||||||
response: str = ""
|
|
||||||
for task in tasks:
|
|
||||||
status: bool = task.cancel()
|
|
||||||
response += f"Task: __{task.get_name()}__\nCancelled: __{status}__\n"
|
|
||||||
await message.reply(response, del_in=5)
|
|
||||||
|
|
||||||
|
|
||||||
if Config.DEV_MODE:
|
|
||||||
Config.CMD_DICT["sh"] = run_cmd
|
|
||||||
Config.CMD_DICT["shell"] = live_shell
|
|
||||||
Config.CMD_DICT["exec"] = executor
|
|
||||||
Config.CMD_DICT["load"] = loader
|
|
92
app/plugins/files/download.py
Normal file
92
app/plugins/files/download.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from app import BOT, bot
|
||||||
|
from app.core import Message
|
||||||
|
from app.utils.downloader import Download, DownloadedFile
|
||||||
|
from app.utils.helpers import progress
|
||||||
|
from app.utils.media_helper import get_tg_media_details
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="download")
|
||||||
|
async def down_load(bot: BOT, message: Message):
|
||||||
|
"""
|
||||||
|
CMD: DOWNLOAD
|
||||||
|
INFO: Download Files/TG Media to Bot server.
|
||||||
|
FLAGS: "-f" for custom filename
|
||||||
|
USAGE:
|
||||||
|
.download URL | Reply to Media
|
||||||
|
.download -f file.ext URL | Reply to Media
|
||||||
|
"""
|
||||||
|
response = await message.reply("Checking Input...")
|
||||||
|
if (not message.replied or not message.replied.media) and not message.input:
|
||||||
|
await response.edit(
|
||||||
|
"Invalid input...\nReply to a message containing media or give a link with cmd."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
dl_path = os.path.join("downloads", str(time.time()))
|
||||||
|
await response.edit("Input verified....Starting Download...")
|
||||||
|
file_name = None
|
||||||
|
if message.replied and message.replied.media:
|
||||||
|
if "-f" in message.flags:
|
||||||
|
file_name = message.flt_input
|
||||||
|
download_coro = telegram_download(
|
||||||
|
message=message.replied,
|
||||||
|
response=response,
|
||||||
|
path=dl_path,
|
||||||
|
file_name=file_name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if "-f" in message.flags:
|
||||||
|
file_name, url = message.flt_input.split()
|
||||||
|
else:
|
||||||
|
url = message.flt_input
|
||||||
|
dl_obj: Download = await Download.setup(
|
||||||
|
url=url, path=dl_path, message_to_edit=response, custom_file_name=file_name
|
||||||
|
)
|
||||||
|
download_coro = dl_obj.download()
|
||||||
|
try:
|
||||||
|
downloaded_file: DownloadedFile = await download_coro
|
||||||
|
await response.edit(
|
||||||
|
f"<b>Download Completed</b>"
|
||||||
|
f"\n<pre language=bash>"
|
||||||
|
f"\nfile={downloaded_file.name}"
|
||||||
|
f"\npath={downloaded_file.full_path}"
|
||||||
|
f"\nsize={downloaded_file.size}mb</pre>"
|
||||||
|
)
|
||||||
|
return downloaded_file
|
||||||
|
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
await response.edit("Cancelled....")
|
||||||
|
except TimeoutError:
|
||||||
|
await response.edit("Download Timeout...")
|
||||||
|
except Exception as e:
|
||||||
|
await response.edit(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
async def telegram_download(
|
||||||
|
message: Message, response: Message, path: str, file_name: str | None = None
|
||||||
|
) -> DownloadedFile:
|
||||||
|
"""
|
||||||
|
:param message: Message Containing Media
|
||||||
|
:param response: Response to Edit
|
||||||
|
:param path: Download path
|
||||||
|
:param file_name: Custom File Name
|
||||||
|
:return: DownloadedFile
|
||||||
|
"""
|
||||||
|
tg_media = get_tg_media_details(message)
|
||||||
|
tg_media.file_name = file_name or tg_media.file_name
|
||||||
|
media_obj: DownloadedFile = DownloadedFile(
|
||||||
|
name=tg_media.file_name,
|
||||||
|
path=path,
|
||||||
|
size=round(tg_media.file_size / 1048576, 1),
|
||||||
|
full_path=os.path.join(path, tg_media.file_name),
|
||||||
|
)
|
||||||
|
progress_args = (response, "Downloading...", media_obj.name, media_obj.full_path)
|
||||||
|
await message.download(
|
||||||
|
file_name=media_obj.full_path,
|
||||||
|
progress=progress,
|
||||||
|
progress_args=progress_args,
|
||||||
|
)
|
||||||
|
return media_obj
|
70
app/plugins/files/rename.py
Normal file
70
app/plugins/files/rename.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
|
|
||||||
|
from app import BOT, bot
|
||||||
|
from app.core import Message
|
||||||
|
from app.plugins.files.download import telegram_download
|
||||||
|
from app.plugins.files.upload import FILE_TYPE_MAP
|
||||||
|
from app.utils.downloader import Download, DownloadedFile
|
||||||
|
from app.utils.helpers import progress
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="rename")
|
||||||
|
async def rename(bot: BOT, message: Message):
|
||||||
|
"""
|
||||||
|
CMD: RENAME
|
||||||
|
INFO: Upload Files with custom name
|
||||||
|
FLAGS: -s for spoiler
|
||||||
|
USAGE:
|
||||||
|
.rename [ url | reply to message ] file_name.ext
|
||||||
|
"""
|
||||||
|
input = message.flt_input
|
||||||
|
response = await message.reply("Checking input...")
|
||||||
|
if (not message.replied or not message.replied.media) and not message.flt_input:
|
||||||
|
await response.edit(
|
||||||
|
"Invalid input...\nReply to a message containing media or give a link and filename with cmd."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
dl_path = os.path.join("downloads", str(time.time()))
|
||||||
|
await response.edit("Input verified....Starting Download...")
|
||||||
|
if message.replied:
|
||||||
|
download_coro = telegram_download(
|
||||||
|
message=message.replied,
|
||||||
|
path=dl_path,
|
||||||
|
file_name=input,
|
||||||
|
response=response,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
url, file_name = input.split()
|
||||||
|
dl_obj: Download = await Download.setup(
|
||||||
|
url=url, path=dl_path, message_to_edit=response, custom_file_name=file_name
|
||||||
|
)
|
||||||
|
download_coro = dl_obj.download()
|
||||||
|
try:
|
||||||
|
downloaded_file: DownloadedFile = await download_coro
|
||||||
|
media: dict = await FILE_TYPE_MAP[downloaded_file.type](
|
||||||
|
downloaded_file, has_spoiler="-s" in message.flags
|
||||||
|
)
|
||||||
|
progress_args = (
|
||||||
|
response,
|
||||||
|
"Uploading...",
|
||||||
|
downloaded_file.name,
|
||||||
|
downloaded_file.full_path,
|
||||||
|
)
|
||||||
|
await media["method"](
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
reply_to_message_id=message.reply_id,
|
||||||
|
progress=progress,
|
||||||
|
progress_args=progress_args,
|
||||||
|
**media["kwargs"]
|
||||||
|
)
|
||||||
|
shutil.rmtree(dl_path, ignore_errors=True)
|
||||||
|
await response.delete()
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
await response.edit("Cancelled....")
|
||||||
|
except TimeoutError:
|
||||||
|
await response.edit("Download Timeout...")
|
||||||
|
except Exception as e:
|
||||||
|
await response.edit(str(e))
|
151
app/plugins/files/upload.py
Normal file
151
app/plugins/files/upload.py
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from app import BOT, Config, bot
|
||||||
|
from app.core import Message
|
||||||
|
from app.utils.downloader import Download, DownloadedFile
|
||||||
|
from app.utils.helpers import progress
|
||||||
|
from app.utils.media_helper import MediaType, bytes_to_mb
|
||||||
|
from app.utils.shell import check_audio, get_duration, take_ss
|
||||||
|
|
||||||
|
|
||||||
|
async def video_upload(
|
||||||
|
file: DownloadedFile, has_spoiler: bool
|
||||||
|
) -> dict[str, bot.send_video, bot.send_animation, dict]:
|
||||||
|
thumb = await take_ss(file.full_path, path=file.path)
|
||||||
|
if not (await check_audio(file.full_path)): # fmt:skip
|
||||||
|
return dict(
|
||||||
|
method=bot.send_animation,
|
||||||
|
kwargs=dict(
|
||||||
|
thumb=thumb,
|
||||||
|
unsave=True,
|
||||||
|
animation=file.full_path,
|
||||||
|
duration=await get_duration(file.full_path),
|
||||||
|
has_spoiler=has_spoiler,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return dict(
|
||||||
|
method=bot.send_video,
|
||||||
|
kwargs=dict(
|
||||||
|
thumb=thumb,
|
||||||
|
video=file.full_path,
|
||||||
|
duration=await get_duration(file.full_path),
|
||||||
|
has_spoiler=has_spoiler,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def photo_upload(
|
||||||
|
file: DownloadedFile, has_spoiler: bool
|
||||||
|
) -> dict[str, bot.send_photo, dict]:
|
||||||
|
return dict(
|
||||||
|
method=bot.send_photo,
|
||||||
|
kwargs=dict(photo=file.full_path, has_spoiler=has_spoiler),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def audio_upload(
|
||||||
|
file: DownloadedFile, has_spoiler: bool
|
||||||
|
) -> dict[str, bot.send_audio, dict]:
|
||||||
|
return dict(
|
||||||
|
method=bot.send_audio,
|
||||||
|
kwargs=dict(
|
||||||
|
audio=file.full_path, duration=await get_duration(file=file.full_path)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def doc_upload(
|
||||||
|
file: DownloadedFile, has_spoiler: bool
|
||||||
|
) -> dict[str, bot.send_document, dict]:
|
||||||
|
return dict(
|
||||||
|
method=bot.send_document,
|
||||||
|
kwargs=dict(document=file.full_path, force_document=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FILE_TYPE_MAP = {
|
||||||
|
MediaType.PHOTO: photo_upload,
|
||||||
|
MediaType.DOCUMENT: doc_upload,
|
||||||
|
MediaType.GIF: video_upload,
|
||||||
|
MediaType.AUDIO: audio_upload,
|
||||||
|
MediaType.VIDEO: video_upload,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def file_check(file: str):
|
||||||
|
return os.path.isfile(file)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="upload")
|
||||||
|
async def upload(bot: BOT, message: Message):
|
||||||
|
"""
|
||||||
|
CMD: UPLOAD
|
||||||
|
INFO: Upload Media/Local Files/Plugins to TG.
|
||||||
|
FLAGS:
|
||||||
|
-d: to upload as doc.
|
||||||
|
-s: spoiler.
|
||||||
|
USAGE:
|
||||||
|
.upload [-d] URL | Path to File | CMD
|
||||||
|
"""
|
||||||
|
input = message.flt_input
|
||||||
|
if not input:
|
||||||
|
await message.reply("give a file url | path to upload.")
|
||||||
|
return
|
||||||
|
response = await message.reply("checking input...")
|
||||||
|
if input in Config.CMD_DICT.keys():
|
||||||
|
await message.reply_document(document=Config.CMD_DICT[input].path)
|
||||||
|
await response.delete()
|
||||||
|
return
|
||||||
|
elif input.startswith("http") and not file_check(input):
|
||||||
|
dl_obj: Download = await Download.setup(
|
||||||
|
url=input,
|
||||||
|
path=os.path.join("downloads", str(time.time())),
|
||||||
|
message_to_edit=response,
|
||||||
|
)
|
||||||
|
if not bot.me.is_premium and dl_obj.size > 1999:
|
||||||
|
await response.edit(
|
||||||
|
"<b>Aborted</b>, File size exceeds 2gb limit for non premium users!!!"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
file: DownloadedFile = await dl_obj.download()
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
await response.edit("Cancelled...")
|
||||||
|
return
|
||||||
|
except TimeoutError:
|
||||||
|
await response.edit("Download Timeout...")
|
||||||
|
return
|
||||||
|
elif file_check(input):
|
||||||
|
file = DownloadedFile(
|
||||||
|
name=input,
|
||||||
|
path=os.path.dirname(input),
|
||||||
|
full_path=input,
|
||||||
|
size=bytes_to_mb(os.path.getsize(input)),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await response.edit("invalid `cmd` | `url` | `file path`!!!")
|
||||||
|
return
|
||||||
|
await response.edit("uploading....")
|
||||||
|
progress_args = (response, "Uploading...", file.name, file.full_path)
|
||||||
|
if "-d" in message.flags:
|
||||||
|
media: dict = dict(
|
||||||
|
method=bot.send_document,
|
||||||
|
kwargs=dict(document=file.full_path, force_document=True),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
media: dict = await FILE_TYPE_MAP[file.type](
|
||||||
|
file, has_spoiler="-s" in message.flags
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await media["method"](
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
reply_to_message_id=message.reply_id,
|
||||||
|
progress=progress,
|
||||||
|
progress_args=progress_args,
|
||||||
|
**media["kwargs"]
|
||||||
|
)
|
||||||
|
await response.delete()
|
||||||
|
except asyncio.exceptions.CancelledError:
|
||||||
|
await response.edit("Cancelled....")
|
@ -1,87 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from pyrogram.enums import ChatType
|
|
||||||
from pyrogram.errors import BadRequest
|
|
||||||
|
|
||||||
from app import Config, Message, bot
|
|
||||||
|
|
||||||
# Delete replied and command message
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="del")
|
|
||||||
async def delete_message(bot: bot, message: Message) -> None:
|
|
||||||
await message.delete(reply=True)
|
|
||||||
|
|
||||||
|
|
||||||
# Delete Multiple messages from replied to command.
|
|
||||||
@bot.add_cmd(cmd="purge")
|
|
||||||
async def purge_(bot: bot, message: Message) -> None | Message:
|
|
||||||
start_message: int = message.reply_id
|
|
||||||
if not start_message:
|
|
||||||
return await message.reply("reply to a message")
|
|
||||||
end_message: int = message.id
|
|
||||||
messages: list[int] = [
|
|
||||||
end_message,
|
|
||||||
*[i for i in range(int(start_message), int(end_message))],
|
|
||||||
]
|
|
||||||
await bot.delete_messages(
|
|
||||||
chat_id=message.chat.id, message_ids=messages, revoke=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="ids")
|
|
||||||
async def get_ids(bot: bot, message: Message) -> None:
|
|
||||||
reply: Message = message.replied
|
|
||||||
if reply:
|
|
||||||
ids: str = ""
|
|
||||||
reply_forward = reply.forward_from_chat
|
|
||||||
reply_user = reply.from_user
|
|
||||||
ids += f"Chat : `{reply.chat.id}`\n"
|
|
||||||
if reply_forward:
|
|
||||||
ids += f"Replied {'Channel' if reply_forward.type == ChatType.CHANNEL else 'Chat'} : `{reply_forward.id}`\n"
|
|
||||||
if reply_user:
|
|
||||||
ids += f"User : {reply.from_user.id}"
|
|
||||||
else:
|
|
||||||
ids: str = f"Chat :`{message.chat.id}`"
|
|
||||||
await message.reply(ids)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="join")
|
|
||||||
async def join_chat(bot: bot, message: Message) -> Message:
|
|
||||||
chat: str = message.input
|
|
||||||
try:
|
|
||||||
await bot.join_chat(chat)
|
|
||||||
except (KeyError, BadRequest):
|
|
||||||
try:
|
|
||||||
await bot.join_chat(os.path.basename(chat).strip())
|
|
||||||
except Exception as e:
|
|
||||||
return await message.reply(str(e))
|
|
||||||
await message.reply("Joined")
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="leave")
|
|
||||||
async def leave_chat(bot: bot, message: Message) -> None:
|
|
||||||
if message.input:
|
|
||||||
chat = message.input
|
|
||||||
else:
|
|
||||||
chat = message.chat.id
|
|
||||||
await message.reply(
|
|
||||||
f"Leaving current chat in 5\nReply with `{Config.TRIGGER}c` to cancel",
|
|
||||||
del_in=5,
|
|
||||||
block=True,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await bot.leave_chat(chat)
|
|
||||||
except Exception as e:
|
|
||||||
await message.reply(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="reply")
|
|
||||||
async def reply(bot: bot, message: Message) -> None:
|
|
||||||
text: str = message.input
|
|
||||||
await bot.send_message(
|
|
||||||
chat_id=message.chat.id,
|
|
||||||
text=text,
|
|
||||||
reply_to_message_id=message.reply_id,
|
|
||||||
disable_web_page_preview=True,
|
|
||||||
)
|
|
@ -1,10 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from app import Message, bot
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="c")
|
@bot.add_cmd(cmd="c")
|
||||||
async def cancel_task(bot: bot, message: Message) -> Message | None:
|
async def cancel_task(bot: BOT, message: Message) -> Message | None:
|
||||||
|
"""
|
||||||
|
CMD: CANCEL
|
||||||
|
INFO: Cancel a running command by replying to a message.
|
||||||
|
USAGE: .c
|
||||||
|
"""
|
||||||
task_id: str | None = message.replied_task_id
|
task_id: str | None = message.replied_task_id
|
||||||
if not task_id:
|
if not task_id:
|
||||||
return await message.reply(
|
return await message.reply(
|
56
app/plugins/tools/chat.py
Normal file
56
app/plugins/tools/chat.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from pyrogram.enums import ChatType
|
||||||
|
from pyrogram.errors import BadRequest
|
||||||
|
|
||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="ids")
|
||||||
|
async def get_ids(bot: BOT, message: Message) -> None:
|
||||||
|
reply: Message = message.replied
|
||||||
|
if reply:
|
||||||
|
ids: str = ""
|
||||||
|
reply_forward = reply.forward_from_chat
|
||||||
|
reply_user = reply.from_user
|
||||||
|
ids += f"<b>Chat</b> : `{reply.chat.id}`\n"
|
||||||
|
if reply_forward:
|
||||||
|
ids += f"<b>Replied {'Channel' if reply_forward.type == ChatType.CHANNEL else 'Chat'}</b> : `{reply_forward.id}`\n"
|
||||||
|
if reply_user:
|
||||||
|
ids += f"<b>User</b> : {reply.from_user.id}"
|
||||||
|
elif message.input:
|
||||||
|
ids: int = (await bot.get_chat(message.input[1:])).id
|
||||||
|
else:
|
||||||
|
ids: str = f"<b>Chat</b> :`{message.chat.id}`"
|
||||||
|
await message.reply(ids)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="join")
|
||||||
|
async def join_chat(bot: BOT, message: Message) -> None:
|
||||||
|
chat: str = message.input
|
||||||
|
try:
|
||||||
|
await bot.join_chat(chat)
|
||||||
|
except (KeyError, BadRequest):
|
||||||
|
try:
|
||||||
|
await bot.join_chat(os.path.basename(chat).strip())
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(str(e))
|
||||||
|
return
|
||||||
|
await message.reply("Joined")
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="leave")
|
||||||
|
async def leave_chat(bot: BOT, message: Message) -> None:
|
||||||
|
if message.input:
|
||||||
|
chat = message.input
|
||||||
|
else:
|
||||||
|
chat = message.chat.id
|
||||||
|
await message.reply(
|
||||||
|
text=f"Leaving current chat in 5\nReply with `{message.trigger}c` to cancel",
|
||||||
|
del_in=5,
|
||||||
|
block=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await bot.leave_chat(chat)
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(str(e))
|
14
app/plugins/tools/click.py
Normal file
14
app/plugins/tools/click.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="click")
|
||||||
|
async def click(bot: BOT, message: Message):
|
||||||
|
if not message.input or not message.replied:
|
||||||
|
await message.reply(
|
||||||
|
"reply to a message containing a button and give a button to click"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await message.replied.click(message.input.strip())
|
||||||
|
except Exception as e:
|
||||||
|
await message.reply(str(e), del_in=5)
|
34
app/plugins/tools/delete.py
Normal file
34
app/plugins/tools/delete.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from app import BOT, bot
|
||||||
|
from app.core import Message
|
||||||
|
from app.plugins.tools.get_message import parse_link
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="del")
|
||||||
|
async def delete_message(bot: BOT, message: Message) -> None:
|
||||||
|
"""
|
||||||
|
CMD: DEL
|
||||||
|
INFO: Delete the replied message.
|
||||||
|
FLAGS: -r to remotely delete a text using its link.
|
||||||
|
USAGE:
|
||||||
|
.del | .del -r t.me/......
|
||||||
|
"""
|
||||||
|
if "-r" in message.flags:
|
||||||
|
chat_id, message_id = parse_link(message.flt_input)
|
||||||
|
await bot.delete_messages(chat_id=chat_id, message_ids=message_id, revoke=True)
|
||||||
|
return
|
||||||
|
await message.delete(reply=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="purge")
|
||||||
|
async def purge_(bot: BOT, message: Message) -> None | Message:
|
||||||
|
start_message: int = message.reply_id
|
||||||
|
if not start_message:
|
||||||
|
return await message.reply("reply to a message")
|
||||||
|
end_message: int = message.id
|
||||||
|
messages: list[int] = [
|
||||||
|
end_message,
|
||||||
|
*[i for i in range(int(start_message), int(end_message))],
|
||||||
|
]
|
||||||
|
await bot.delete_messages(
|
||||||
|
chat_id=message.chat.id, message_ids=messages, revoke=True
|
||||||
|
)
|
36
app/plugins/tools/get_message.py
Normal file
36
app/plugins/tools/get_message.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
def parse_link(link: str) -> tuple[int | str, int]:
|
||||||
|
parsed_url: str = urlparse(link).path.strip("/")
|
||||||
|
chat, id = parsed_url.lstrip("c/").split("/")
|
||||||
|
if chat.isdigit():
|
||||||
|
chat = int(f"-100{chat}")
|
||||||
|
return chat, int(id)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="gm")
|
||||||
|
async def get_message(bot: BOT, message: Message):
|
||||||
|
"""
|
||||||
|
CMD: Get Message
|
||||||
|
INFO: Get a Message Json/Attr by providing link.
|
||||||
|
USAGE:
|
||||||
|
.gm t.me/.... | .gm t.me/... text [Returns message text]
|
||||||
|
"""
|
||||||
|
if not message.input:
|
||||||
|
await message.reply("Give a Message link.")
|
||||||
|
attr = None
|
||||||
|
if len(message.text_list) == 3:
|
||||||
|
link, attr = message.text_list[1:]
|
||||||
|
else:
|
||||||
|
link = message.input.strip()
|
||||||
|
remote_message = await bot.get_messages(*parse_link(link))
|
||||||
|
if not attr:
|
||||||
|
await message.reply(str(remote_message))
|
||||||
|
return
|
||||||
|
if hasattr(remote_message, attr):
|
||||||
|
await message.reply(str(getattr(remote_message, attr)))
|
||||||
|
return
|
||||||
|
await message.reply(f"Message object has no attribute '{attr}'")
|
32
app/plugins/tools/reply.py
Normal file
32
app/plugins/tools/reply.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from app import BOT, Message, bot
|
||||||
|
from app.plugins.tools.get_message import parse_link
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="reply")
|
||||||
|
async def reply(bot: BOT, message: Message) -> None:
|
||||||
|
"""
|
||||||
|
CMD: REPLY
|
||||||
|
INFO: Reply to a Message.
|
||||||
|
FLAGS: -r to reply remotely using message link.
|
||||||
|
USAGE:
|
||||||
|
.reply HI | .reply -r t.me/... HI
|
||||||
|
"""
|
||||||
|
if "-r" in message.flags:
|
||||||
|
input: list[str] = message.flt_input.split(" ", maxsplit=1)
|
||||||
|
if len(input) < 2:
|
||||||
|
await message.reply("The '-r' flag requires a message link and text.")
|
||||||
|
return
|
||||||
|
message_link, text = input
|
||||||
|
chat_id, reply_to_message_id = parse_link(message_link.strip())
|
||||||
|
else:
|
||||||
|
text: str = message.input
|
||||||
|
chat_id = message.chat.id
|
||||||
|
reply_to_message_id = message.reply_id
|
||||||
|
if not text:
|
||||||
|
return
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=chat_id,
|
||||||
|
text=text,
|
||||||
|
reply_to_message_id=reply_to_message_id,
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
@ -1,88 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import os
|
|
||||||
|
|
||||||
from async_lru import alru_cache
|
|
||||||
from git import Repo
|
|
||||||
from pyrogram.enums import ChatType
|
|
||||||
|
|
||||||
from app import Config, Message, bot
|
|
||||||
|
|
||||||
|
|
||||||
@alru_cache()
|
|
||||||
async def get_banner() -> Message:
|
|
||||||
return await bot.get_messages("Social_DL", [2, 3])
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="bot")
|
|
||||||
async def info(bot, message):
|
|
||||||
head = "<b><a href=https://t.me/Social_DL>Social-DL</a> is running.</b>"
|
|
||||||
chat_count = (
|
|
||||||
f"\n<b>Auto-Dl enabled in: <code>{len(Config.CHATS)}</code> chats</b>\n"
|
|
||||||
)
|
|
||||||
supported_sites, photo = await get_banner()
|
|
||||||
await photo.copy(
|
|
||||||
message.chat.id,
|
|
||||||
caption="\n".join([head, chat_count, supported_sites.text.html]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="help")
|
|
||||||
async def cmd_list(bot: bot, message: Message) -> None:
|
|
||||||
commands: str = "\n".join(
|
|
||||||
[f"<code>{Config.TRIGGER}{i}</code>" for i in Config.CMD_DICT.keys()]
|
|
||||||
)
|
|
||||||
await message.reply(f"<b>Available Commands:</b>\n\n{commands}", del_in=30)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="restart")
|
|
||||||
async def restart(bot: bot, message: Message, u_resp: Message | None = None) -> None:
|
|
||||||
reply: Message = u_resp or await message.reply("restarting....")
|
|
||||||
if reply.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP):
|
|
||||||
os.environ["RESTART_MSG"] = str(reply.id)
|
|
||||||
os.environ["RESTART_CHAT"] = str(reply.chat.id)
|
|
||||||
await bot.restart(hard="-h" in message.flags)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="refresh")
|
|
||||||
async def chat_update(bot: bot, message: Message) -> None:
|
|
||||||
await bot.set_filter_list()
|
|
||||||
await message.reply("Filters Refreshed", del_in=8)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="repo")
|
|
||||||
async def sauce(bot: bot, message: Message) -> None:
|
|
||||||
await bot.send_message(
|
|
||||||
chat_id=message.chat.id,
|
|
||||||
text="<a href='https://github.com/anonymousx97/social-dl'>Social-DL</a>",
|
|
||||||
reply_to_message_id=message.reply_id or message.id,
|
|
||||||
disable_web_page_preview=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bot.add_cmd(cmd="update")
|
|
||||||
async def updater(bot: bot, message: Message) -> None | Message:
|
|
||||||
reply: Message = await message.reply("Checking for Updates....")
|
|
||||||
repo: Repo = Repo()
|
|
||||||
repo.git.fetch()
|
|
||||||
commits: str = ""
|
|
||||||
limit: int = 0
|
|
||||||
for commit in repo.iter_commits("HEAD..origin/main"):
|
|
||||||
commits += f"""
|
|
||||||
<b>#{commit.count()}</b> <a href='{Config.UPSTREAM_REPO}/commit/{commit}'>{commit.summary}</a> By <i>{commit.author}</i>
|
|
||||||
"""
|
|
||||||
limit += 1
|
|
||||||
if limit > 50:
|
|
||||||
break
|
|
||||||
if not commits:
|
|
||||||
return await reply.edit(text="Already Up To Date.", del_in=5)
|
|
||||||
if "-pull" not in message.flags:
|
|
||||||
return await reply.edit(
|
|
||||||
text=f"<b>Update Available:</b>\n\n{commits}", disable_web_page_preview=True
|
|
||||||
)
|
|
||||||
repo.git.reset("--hard")
|
|
||||||
repo.git.pull(Config.UPSTREAM_REPO, "--rebase=true")
|
|
||||||
await asyncio.gather(
|
|
||||||
bot.log(text=f"#Updater\nPulled:\n\n{commits}", disable_web_page_preview=True),
|
|
||||||
reply.edit("<b>Update Found</b>\n<i>Pulling....</i>"),
|
|
||||||
)
|
|
||||||
await restart(bot, message, reply)
|
|
27
app/plugins/utils/cmdinfo.py
Normal file
27
app/plugins/utils/cmdinfo.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from app import BOT, Config, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="ci")
|
||||||
|
async def cmd_info(bot: BOT, message: Message):
|
||||||
|
"""
|
||||||
|
CMD: CI (CMD INFO)
|
||||||
|
INFO: Get Github File URL of a Command.
|
||||||
|
USAGE: .ci ci
|
||||||
|
"""
|
||||||
|
cmd = message.flt_input
|
||||||
|
if not cmd or cmd not in Config.CMD_DICT.keys():
|
||||||
|
await message.reply("Give a valid cmd.", del_in=5)
|
||||||
|
return
|
||||||
|
cmd_path = Config.CMD_DICT[cmd].path
|
||||||
|
plugin_path = os.path.relpath(cmd_path, os.curdir)
|
||||||
|
repo = Config.REPO.remotes.origin.url
|
||||||
|
branch = Config.REPO.active_branch
|
||||||
|
remote_url = os.path.join(str(repo), "blob", str(branch), plugin_path)
|
||||||
|
resp_str = (
|
||||||
|
f"<pre language=bash>CMD={cmd}"
|
||||||
|
f"\nLocal_Path={cmd_path}</pre>"
|
||||||
|
f"\nLink: <a href='{remote_url}'>Github</a>"
|
||||||
|
)
|
||||||
|
await message.reply(resp_str, disable_web_page_preview=True)
|
30
app/plugins/utils/help.py
Normal file
30
app/plugins/utils/help.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from app import BOT, Config, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="help")
|
||||||
|
async def cmd_list(bot: BOT, message: Message) -> None:
|
||||||
|
"""
|
||||||
|
CMD: HELP
|
||||||
|
INFO: Check info/about available cmds.
|
||||||
|
USAGE:
|
||||||
|
.help help | .help
|
||||||
|
"""
|
||||||
|
cmd = message.input.strip()
|
||||||
|
if not cmd:
|
||||||
|
commands: str = " ".join(
|
||||||
|
[
|
||||||
|
f"<code>{message.trigger}{cmd}</code>"
|
||||||
|
for cmd in sorted(Config.CMD_DICT.keys())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
await message.reply(
|
||||||
|
text=f"<b>Available Commands:</b>\n\n{commands}", del_in=30, block=True
|
||||||
|
)
|
||||||
|
elif cmd not in Config.CMD_DICT.keys():
|
||||||
|
await message.reply(
|
||||||
|
f"Invalid <b>{cmd}</b>, check {message.trigger}help", del_in=5
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await message.reply(
|
||||||
|
f"<pre language=js>Doc:{Config.CMD_DICT[cmd].doc}</pre>", del_in=30
|
||||||
|
)
|
14
app/plugins/utils/logs.py
Normal file
14
app/plugins/utils/logs.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import aiofiles
|
||||||
|
|
||||||
|
from app import BOT, bot
|
||||||
|
from app.core import Message
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="logs")
|
||||||
|
async def read_logs(bot: BOT, message: Message):
|
||||||
|
async with aiofiles.open("logs/app_logs.txt", "r") as aio_file:
|
||||||
|
text = await aio_file.read()
|
||||||
|
if len(text) < 4050:
|
||||||
|
await message.reply(f"<pre language=bash>{text}</pre>")
|
||||||
|
else:
|
||||||
|
await message.reply_document(document="logs/app_logs.txt")
|
13
app/plugins/utils/ping.py
Normal file
13
app/plugins/utils/ping.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
# Not my Code
|
||||||
|
# Prolly from Userge/UX/VenomX IDK
|
||||||
|
@bot.add_cmd(cmd="ping")
|
||||||
|
async def ping_bot(bot: BOT, message: Message):
|
||||||
|
start = datetime.now()
|
||||||
|
resp: Message = await message.reply("Checking Ping.....")
|
||||||
|
end = (datetime.now() - start).microseconds / 1000
|
||||||
|
await resp.edit(f"Pong! {end} ms.")
|
11
app/plugins/utils/repo.py
Normal file
11
app/plugins/utils/repo.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="repo")
|
||||||
|
async def sauce(bot: BOT, message: Message) -> None:
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=message.chat.id,
|
||||||
|
text="<a href='https://github.com/anonymousx97/social-dl'>Social-DL</a>",
|
||||||
|
reply_to_message_id=message.reply_id or message.id,
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
21
app/plugins/utils/restart.py
Normal file
21
app/plugins/utils/restart.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from pyrogram.enums import ChatType
|
||||||
|
|
||||||
|
from app import BOT, Message, bot
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="restart")
|
||||||
|
async def restart(bot: BOT, message: Message, u_resp: Message | None = None) -> None:
|
||||||
|
"""
|
||||||
|
CMD: RESTART
|
||||||
|
INFO: Restart the Bot.
|
||||||
|
FLAGS: -h for hard restart and clearing logs
|
||||||
|
Usage:
|
||||||
|
.restart | .restart -h
|
||||||
|
"""
|
||||||
|
reply: Message = u_resp or await message.reply("restarting....")
|
||||||
|
if reply.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP):
|
||||||
|
os.environ["RESTART_MSG"] = str(reply.id)
|
||||||
|
os.environ["RESTART_CHAT"] = str(reply.chat.id)
|
||||||
|
await bot.restart(hard="-h" in message.flags)
|
69
app/plugins/utils/update.py
Normal file
69
app/plugins/utils/update.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from git import Repo
|
||||||
|
|
||||||
|
from app import BOT, Config, Message, bot
|
||||||
|
from app.plugins.utils.restart import restart
|
||||||
|
|
||||||
|
|
||||||
|
async def get_commits(repo: Repo) -> str | None:
|
||||||
|
try:
|
||||||
|
async with asyncio.timeout(10):
|
||||||
|
await asyncio.to_thread(repo.git.fetch)
|
||||||
|
except TimeoutError:
|
||||||
|
return
|
||||||
|
commits: str = ""
|
||||||
|
limit: int = 0
|
||||||
|
for commit in repo.iter_commits("HEAD..origin/main"):
|
||||||
|
commits += f"""
|
||||||
|
<b>#{commit.count()}</b> <a href='{Config.UPSTREAM_REPO}/commit/{commit}'>{commit.message}</a> By <i>{commit.author}</i>
|
||||||
|
"""
|
||||||
|
limit += 1
|
||||||
|
if limit > 50:
|
||||||
|
break
|
||||||
|
return commits
|
||||||
|
|
||||||
|
|
||||||
|
async def pull_commits(repo: Repo) -> None | bool:
|
||||||
|
repo.git.reset("--hard")
|
||||||
|
try:
|
||||||
|
async with asyncio.timeout(10):
|
||||||
|
await asyncio.to_thread(
|
||||||
|
repo.git.pull, Config.UPSTREAM_REPO, "--rebase=true"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except TimeoutError:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="update")
|
||||||
|
async def updater(bot: BOT, message: Message) -> None | Message:
|
||||||
|
"""
|
||||||
|
CMD: UPDATE
|
||||||
|
INFO: Pull / Check for updates.
|
||||||
|
FLAGS: -pull to pull updates
|
||||||
|
USAGE:
|
||||||
|
.update | .update -pull
|
||||||
|
"""
|
||||||
|
reply: Message = await message.reply("Checking for Updates....")
|
||||||
|
repo: Repo = Config.REPO
|
||||||
|
commits: str = await get_commits(repo)
|
||||||
|
if commits is None:
|
||||||
|
await reply.edit("Timeout... Try again.")
|
||||||
|
return
|
||||||
|
if not commits:
|
||||||
|
await reply.edit("Already Up To Date.", del_in=5)
|
||||||
|
return
|
||||||
|
if "-pull" not in message.flags:
|
||||||
|
await reply.edit(
|
||||||
|
f"<b>Update Available:</b>\n{commits}", disable_web_page_preview=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if not (await pull_commits(repo)): # NOQA
|
||||||
|
await reply.edit("Timeout...Try again.")
|
||||||
|
return
|
||||||
|
await asyncio.gather(
|
||||||
|
bot.log(text=f"#Updater\nPulled:\n{commits}", disable_web_page_preview=True),
|
||||||
|
reply.edit("<b>Update Found</b>\n<i>Pulling....</i>"),
|
||||||
|
)
|
||||||
|
await restart(bot, message, reply)
|
@ -3,14 +3,14 @@ import glob
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from app.core import shell
|
from app.social_dl.scraper_config import ScraperConfig
|
||||||
from app.core.scraper_config import MediaType, ScraperConfig
|
from app.utils import shell
|
||||||
|
from app.utils.media_helper import MediaType
|
||||||
|
|
||||||
|
|
||||||
class GalleryDL(ScraperConfig):
|
class GalleryDL(ScraperConfig):
|
||||||
def __init__(self, url: str):
|
def __init__(self, url: str):
|
||||||
super().__init__()
|
super().__init__(url=url)
|
||||||
self.url: str = url
|
|
||||||
|
|
||||||
async def download_or_extract(self):
|
async def download_or_extract(self):
|
||||||
self.path: str = "downloads/" + str(time.time())
|
self.path: str = "downloads/" + str(time.time())
|
||||||
@ -18,7 +18,7 @@ class GalleryDL(ScraperConfig):
|
|||||||
try:
|
try:
|
||||||
async with asyncio.timeout(30):
|
async with asyncio.timeout(30):
|
||||||
await shell.run_shell_cmd(
|
await shell.run_shell_cmd(
|
||||||
f"gallery-dl -q --range '0-4' -D {self.path} '{self.url}'"
|
f"gallery-dl -q --range '0-4' -D {self.path} '{self.raw_url}'"
|
||||||
)
|
)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
pass
|
pass
|
130
app/social_dl/api/instagram.py
Executable file
130
app/social_dl/api/instagram.py
Executable file
@ -0,0 +1,130 @@
|
|||||||
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from app import LOGGER, Config, Message, bot
|
||||||
|
from app.social_dl.scraper_config import ScraperConfig
|
||||||
|
from app.utils import aiohttp_tools as aio
|
||||||
|
from app.utils.media_helper import MediaType, get_type
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeys:
|
||||||
|
def __init__(self):
|
||||||
|
self.API2_KEYS: list = Config.API_KEYS
|
||||||
|
self.api_2 = 0
|
||||||
|
|
||||||
|
# Rotating Key function to avoid hitting limit on single Key
|
||||||
|
def get_key(self, func: str) -> str:
|
||||||
|
keys = self.API2_KEYS
|
||||||
|
count = getattr(self, func) + 1
|
||||||
|
if count >= len(keys):
|
||||||
|
count = 0
|
||||||
|
setattr(self, func, count)
|
||||||
|
return keys[count]
|
||||||
|
|
||||||
|
# def switch(self) -> int:
|
||||||
|
# self.switch_val += 1
|
||||||
|
# if self.switch_val >= 3:
|
||||||
|
# self.switch_val = 0
|
||||||
|
# return self.switch_val
|
||||||
|
|
||||||
|
|
||||||
|
api_keys: ApiKeys = ApiKeys()
|
||||||
|
|
||||||
|
|
||||||
|
class Instagram(ScraperConfig):
|
||||||
|
def __init__(self, url):
|
||||||
|
self.APIS = (
|
||||||
|
"check_dump",
|
||||||
|
"no_api_dl",
|
||||||
|
"api_2",
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(url=url)
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
self.shortcode: str = os.path.basename(parsed_url.path.rstrip("/"))
|
||||||
|
self.api_url = f"https://www.instagram.com/graphql/query?query_hash=2b0673e0dc4580674a88d426fe00ea90&variables=%7B%22shortcode%22%3A%22{self.shortcode}%22%7D"
|
||||||
|
self.dump: bool = True
|
||||||
|
|
||||||
|
async def check_dump(self) -> None | bool:
|
||||||
|
if not Config.DUMP_ID:
|
||||||
|
return
|
||||||
|
async for message in bot.search_messages(Config.DUMP_ID, "#" + self.shortcode):
|
||||||
|
self.media: Message = message
|
||||||
|
self.type: MediaType = MediaType.MESSAGE
|
||||||
|
self.in_dump: bool = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def download_or_extract(self) -> None:
|
||||||
|
for api in self.APIS:
|
||||||
|
func = getattr(self, api)
|
||||||
|
if await func():
|
||||||
|
self.success: bool = True
|
||||||
|
break
|
||||||
|
|
||||||
|
async def no_api_dl(self):
|
||||||
|
response: dict | None = await aio.get_json(url=self.api_url)
|
||||||
|
if (
|
||||||
|
not response
|
||||||
|
or "data" not in response
|
||||||
|
or not response["data"]["shortcode_media"]
|
||||||
|
):
|
||||||
|
LOGGER.error(response)
|
||||||
|
return
|
||||||
|
return await self.parse_ghraphql(response["data"]["shortcode_media"])
|
||||||
|
|
||||||
|
async def api_2(self) -> bool | None:
|
||||||
|
if not Config.API_KEYS:
|
||||||
|
return
|
||||||
|
# "/?__a=1&__d=1"
|
||||||
|
response: dict | None = await aio.get_json(
|
||||||
|
url="https://api.webscraping.ai/html",
|
||||||
|
timeout=30,
|
||||||
|
params={
|
||||||
|
"api_key": api_keys.get_key("api_2"),
|
||||||
|
"url": self.api_url,
|
||||||
|
"proxy": "residential",
|
||||||
|
"js": "false",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not response
|
||||||
|
or "data" not in response.keys()
|
||||||
|
or not response["data"]["shortcode_media"]
|
||||||
|
):
|
||||||
|
LOGGER.error(response)
|
||||||
|
return
|
||||||
|
self.caption = ".."
|
||||||
|
return await self.parse_ghraphql(response["data"]["shortcode_media"])
|
||||||
|
|
||||||
|
async def parse_ghraphql(self, json_: dict) -> str | list | None:
|
||||||
|
type_check: str | None = json_.get("__typename", None)
|
||||||
|
if not type_check:
|
||||||
|
return
|
||||||
|
elif type_check == "GraphSidecar":
|
||||||
|
self.media: list[str] = [
|
||||||
|
i["node"].get("video_url") or i["node"].get("display_url")
|
||||||
|
for i in json_["edge_sidecar_to_children"]["edges"]
|
||||||
|
]
|
||||||
|
self.type: MediaType = MediaType.GROUP
|
||||||
|
else:
|
||||||
|
self.media: str = json_.get("video_url", json_.get("display_url"))
|
||||||
|
self.thumb: str = json_.get("display_url")
|
||||||
|
self.type: MediaType = get_type(self.media)
|
||||||
|
return self.media
|
||||||
|
|
||||||
|
async def parse_v2_json(self, data: dict):
|
||||||
|
if data.get("carousel_media"):
|
||||||
|
self.media = []
|
||||||
|
for media in data["carousel_media"]:
|
||||||
|
if media.get("video_dash_manifest"):
|
||||||
|
self.media.append(media["video_versions"][0]["url"])
|
||||||
|
else:
|
||||||
|
self.media.append(media["image_versions2"]["candidates"][0]["url"])
|
||||||
|
self.type = MediaType.GROUP
|
||||||
|
elif data.get("video_dash_manifest"):
|
||||||
|
self.media = data["video_versions"][0]["url"]
|
||||||
|
self.type = MediaType.VIDEO
|
||||||
|
else:
|
||||||
|
self.media = data["image_versions2"]["candidates"][0]["url"]
|
||||||
|
self.type = MediaType.PHOTO
|
||||||
|
return 1
|
@ -3,14 +3,14 @@ import re
|
|||||||
import time
|
import time
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from app.core import shell
|
from app.social_dl.scraper_config import ScraperConfig
|
||||||
from app.core import aiohttp_tools
|
from app.utils import aiohttp_tools, shell
|
||||||
from app.core.scraper_config import MediaType, ScraperConfig
|
from app.utils.media_helper import MediaType, get_type
|
||||||
|
|
||||||
|
|
||||||
class Reddit(ScraperConfig):
|
class Reddit(ScraperConfig):
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
super().__init__()
|
super().__init__(url=url)
|
||||||
parsed_url = urlparse(url)
|
parsed_url = urlparse(url)
|
||||||
self.url: str = f"https://www.reddit.com{parsed_url.path}"
|
self.url: str = f"https://www.reddit.com{parsed_url.path}"
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ class Reddit(ScraperConfig):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
json_: dict = json_data[0]["data"]["children"][0]["data"]
|
json_: dict = json_data[0]["data"]["children"][0]["data"]
|
||||||
except BaseException:
|
except (KeyError, IndexError, ValueError):
|
||||||
return
|
return
|
||||||
|
|
||||||
self.caption: str = (
|
self.caption: str = (
|
||||||
@ -59,7 +59,7 @@ class Reddit(ScraperConfig):
|
|||||||
return
|
return
|
||||||
|
|
||||||
generic: str = json_.get("url_overridden_by_dest", "").strip()
|
generic: str = json_.get("url_overridden_by_dest", "").strip()
|
||||||
self.type: MediaType = aiohttp_tools.get_type(generic)
|
self.type: MediaType = get_type(generic)
|
||||||
if self.type:
|
if self.type:
|
||||||
self.media: str = generic
|
self.media: str = generic
|
||||||
self.success = True
|
self.success = True
|
||||||
@ -72,7 +72,8 @@ class Reddit(ScraperConfig):
|
|||||||
url=f"{self.url}.json?limit=1", headers=headers, json_=True
|
url=f"{self.url}.json?limit=1", headers=headers, json_=True
|
||||||
)
|
)
|
||||||
if not response:
|
if not response:
|
||||||
raw_url = (await aiohttp_tools.SESSION.get(self.url)).url # fmt : skip
|
# fmt : skip
|
||||||
|
raw_url = (await aiohttp_tools.SESSION.get(self.url)).url
|
||||||
parsed_url = urlparse(f"{raw_url}")
|
parsed_url = urlparse(f"{raw_url}")
|
||||||
url: str = f"https://www.reddit.com{parsed_url.path}"
|
url: str = f"https://www.reddit.com{parsed_url.path}"
|
||||||
|
|
@ -3,17 +3,17 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from app.core import aiohttp_tools
|
from app.social_dl.scraper_config import ScraperConfig
|
||||||
from app.core.scraper_config import MediaType, ScraperConfig
|
from app.utils import aiohttp_tools
|
||||||
|
from app.utils.media_helper import MediaType
|
||||||
|
|
||||||
|
|
||||||
class Threads(ScraperConfig):
|
class Threads(ScraperConfig):
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
super().__init__()
|
super().__init__(url=url)
|
||||||
self.url: str = url
|
|
||||||
|
|
||||||
async def download_or_extract(self):
|
async def download_or_extract(self):
|
||||||
shortcode: str = os.path.basename(urlparse(self.url).path.rstrip("/"))
|
shortcode: str = os.path.basename(urlparse(self.raw_url).path.rstrip("/"))
|
||||||
|
|
||||||
response: str = await (
|
response: str = await (
|
||||||
await aiohttp_tools.SESSION.get(
|
await aiohttp_tools.SESSION.get(
|
@ -1,16 +1,16 @@
|
|||||||
from app.api.tiktok_scraper import Scraper as Tiktok_Scraper
|
from app.social_dl.api.tiktok_scraper import Scraper as Tiktok_Scraper
|
||||||
from app.core.scraper_config import MediaType, ScraperConfig
|
from app.social_dl.scraper_config import ScraperConfig
|
||||||
|
from app.utils.media_helper import MediaType
|
||||||
|
|
||||||
tiktok_scraper = Tiktok_Scraper(quiet=True)
|
tiktok_scraper = Tiktok_Scraper(quiet=True)
|
||||||
|
|
||||||
|
|
||||||
class Tiktok(ScraperConfig):
|
class Tiktok(ScraperConfig):
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
super().__init__()
|
super().__init__(url=url)
|
||||||
self.url: str = url
|
|
||||||
|
|
||||||
async def download_or_extract(self):
|
async def download_or_extract(self):
|
||||||
media: dict | None = await tiktok_scraper.hybrid_parsing(self.url)
|
media: dict | None = await tiktok_scraper.hybrid_parsing(self.raw_url)
|
||||||
if not media or "status" not in media or media["status"] == "failed":
|
if not media or "status" not in media or media["status"] == "failed":
|
||||||
return
|
return
|
||||||
if "video_data" in media:
|
if "video_data" in media:
|
@ -4,12 +4,14 @@ import time
|
|||||||
|
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
|
||||||
from app.core.scraper_config import MediaType, ScraperConfig
|
from app.social_dl.scraper_config import ScraperConfig
|
||||||
from app.core.shell import take_ss
|
from app.utils.media_helper import MediaType
|
||||||
|
from app.utils.shell import take_ss
|
||||||
|
|
||||||
# To disable YT-DLP logging
|
# To disable YT-DLP logging
|
||||||
# https://github.com/ytdl-org/youtube-dl/blob/fa7f0effbe4e14fcf70e1dc4496371c9862b64b9/test/helper.py#L92
|
# https://github.com/ytdl-org/youtube-dl/blob/fa7f0effbe4e14fcf70e1dc4496371c9862b64b9/test/helper.py#L92
|
||||||
|
|
||||||
|
|
||||||
class FakeLogger(object):
|
class FakeLogger(object):
|
||||||
def debug(self, msg):
|
def debug(self, msg):
|
||||||
pass
|
pass
|
||||||
@ -23,8 +25,7 @@ class FakeLogger(object):
|
|||||||
|
|
||||||
class YouTubeDL(ScraperConfig):
|
class YouTubeDL(ScraperConfig):
|
||||||
def __init__(self, url):
|
def __init__(self, url):
|
||||||
super().__init__()
|
super().__init__(url=url)
|
||||||
self.url: str = url
|
|
||||||
self.path: str = "downloads/" + str(time.time())
|
self.path: str = "downloads/" + str(time.time())
|
||||||
self.video_path: str = self.path + "/v.mp4"
|
self.video_path: str = self.path + "/v.mp4"
|
||||||
self.type = MediaType.VIDEO
|
self.type = MediaType.VIDEO
|
||||||
@ -44,9 +45,9 @@ class YouTubeDL(ScraperConfig):
|
|||||||
if not info:
|
if not info:
|
||||||
return
|
return
|
||||||
|
|
||||||
await asyncio.to_thread(self.yt_obj.download, self.url)
|
await asyncio.to_thread(self.yt_obj.download, self.raw_url)
|
||||||
|
|
||||||
if "youtu" in self.url:
|
if "youtu" in self.raw_url:
|
||||||
self.caption: str = (
|
self.caption: str = (
|
||||||
f"""__{info.get("channel","")}__:\n**{info.get("title","")}**"""
|
f"""__{info.get("channel","")}__:\n**{info.get("title","")}**"""
|
||||||
)
|
)
|
||||||
@ -57,10 +58,13 @@ class YouTubeDL(ScraperConfig):
|
|||||||
self.success = True
|
self.success = True
|
||||||
|
|
||||||
async def get_info(self) -> None | dict:
|
async def get_info(self) -> None | dict:
|
||||||
if os.path.basename(self.url).startswith("@") or "/hashtag/" in self.url:
|
if (
|
||||||
|
os.path.basename(self.raw_url).startswith("@")
|
||||||
|
or "/hashtag/" in self.raw_url
|
||||||
|
):
|
||||||
return
|
return
|
||||||
info = await asyncio.to_thread(
|
info = await asyncio.to_thread(
|
||||||
self.yt_obj.extract_info, self.url, download=False
|
self.yt_obj.extract_info, self.raw_url, download=False
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not info
|
not info
|
||||||
@ -71,9 +75,9 @@ class YouTubeDL(ScraperConfig):
|
|||||||
return info
|
return info
|
||||||
|
|
||||||
def get_format(self) -> str:
|
def get_format(self) -> str:
|
||||||
if "/shorts" in self.url:
|
if "/shorts" in self.raw_url:
|
||||||
return "bv[ext=mp4][res=720]+ba[ext=m4a]/b[ext=mp4]"
|
return "bv[ext=mp4][res=720]+ba[ext=m4a]/b[ext=mp4]"
|
||||||
elif "youtu" in self.url:
|
elif "youtu" in self.raw_url:
|
||||||
return "bv[ext=mp4][res=480]+ba[ext=m4a]/b[ext=mp4]"
|
return "bv[ext=mp4][res=480]+ba[ext=m4a]/b[ext=mp4]"
|
||||||
else:
|
else:
|
||||||
return "b[ext=mp4]"
|
return "b[ext=mp4]"
|
@ -1,8 +1,32 @@
|
|||||||
|
import json
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from pyrogram.errors import MessageNotModified
|
from pyrogram.errors import MessageNotModified
|
||||||
|
|
||||||
from app import Config, Message, bot
|
from app import Config, bot
|
||||||
|
from app.core import Message
|
||||||
|
|
||||||
|
|
||||||
|
async def init_task():
|
||||||
|
chats_id = Config.AUTO_DL_MESSAGE_ID
|
||||||
|
blocked_id = Config.BLOCKED_USERS_MESSAGE_ID
|
||||||
|
users = Config.USERS_MESSAGE_ID
|
||||||
|
disabled = Config.DISABLED_CHATS_MESSAGE_ID
|
||||||
|
|
||||||
|
if chats_id:
|
||||||
|
Config.CHATS = json.loads(
|
||||||
|
(await bot.get_messages(Config.LOG_CHAT, chats_id)).text
|
||||||
|
)
|
||||||
|
if blocked_id:
|
||||||
|
Config.BLOCKED_USERS = json.loads(
|
||||||
|
(await bot.get_messages(Config.LOG_CHAT, blocked_id)).text
|
||||||
|
)
|
||||||
|
if users:
|
||||||
|
Config.USERS = json.loads((await bot.get_messages(Config.LOG_CHAT, users)).text)
|
||||||
|
if disabled:
|
||||||
|
Config.DISABLED_CHATS = json.loads(
|
||||||
|
(await bot.get_messages(Config.LOG_CHAT, disabled)).text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def add_or_remove(
|
async def add_or_remove(
|
229
app/social_dl/media_handler.py
Normal file
229
app/social_dl/media_handler.py
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import asyncio
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from functools import lru_cache
|
||||||
|
from io import BytesIO
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from pyrogram.enums import ChatType
|
||||||
|
from pyrogram.errors import (
|
||||||
|
MediaEmpty,
|
||||||
|
PhotoSaveFileInvalid,
|
||||||
|
WebpageCurlFailed,
|
||||||
|
WebpageMediaEmpty,
|
||||||
|
)
|
||||||
|
from pyrogram.types import InputMediaPhoto, InputMediaVideo
|
||||||
|
|
||||||
|
from app import Config, bot
|
||||||
|
from app.core import Message
|
||||||
|
from app.social_dl.api.gallery_dl import GalleryDL
|
||||||
|
from app.social_dl.api.instagram import Instagram
|
||||||
|
from app.social_dl.api.reddit import Reddit
|
||||||
|
from app.social_dl.api.threads import Threads
|
||||||
|
|
||||||
|
from app.social_dl.api.tiktok import Tiktok
|
||||||
|
from app.social_dl.api.ytdl import YouTubeDL
|
||||||
|
from app.utils import aiohttp_tools, shell
|
||||||
|
from app.utils.media_helper import MediaExts, MediaType
|
||||||
|
|
||||||
|
url_map: dict = {
|
||||||
|
"tiktok.com": Tiktok,
|
||||||
|
"www.instagram.com": Instagram,
|
||||||
|
"www.reddit.com": Reddit,
|
||||||
|
"reddit.com": Reddit,
|
||||||
|
"www.threads.net": Threads,
|
||||||
|
"twitter.com": GalleryDL,
|
||||||
|
"x.com": GalleryDL,
|
||||||
|
"www.x.com": GalleryDL,
|
||||||
|
"youtube.com": YouTubeDL,
|
||||||
|
"youtu.be": YouTubeDL,
|
||||||
|
"www.facebook.com": YouTubeDL,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_url_dict_items():
|
||||||
|
return url_map.items()
|
||||||
|
|
||||||
|
|
||||||
|
class MediaHandler:
|
||||||
|
def __init__(self, message: Message) -> None:
|
||||||
|
self.exceptions = []
|
||||||
|
self.media_objects: list[
|
||||||
|
Tiktok | Instagram | Reddit | Threads | GalleryDL | YouTubeDL
|
||||||
|
] = []
|
||||||
|
self.sender_dict = {}
|
||||||
|
self.message: Message = message
|
||||||
|
self.doc: bool = "-d" in message.flags
|
||||||
|
self.spoiler: bool = "-s" in message.flags
|
||||||
|
self.args_: dict = {
|
||||||
|
"chat_id": self.message.chat.id,
|
||||||
|
"reply_to_message_id": message.reply_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return json.dumps(self.__dict__, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
|
def get_sender(self, reply: bool = False) -> str:
|
||||||
|
if "-ns" in self.message.flags:
|
||||||
|
return ""
|
||||||
|
text: str = f"\nShared by : "
|
||||||
|
if self.message.chat.type == ChatType.CHANNEL:
|
||||||
|
author: str | None = self.message.author_signature
|
||||||
|
return text + author if author else ""
|
||||||
|
elif reply and self.message.replied and self.message.replied.from_user:
|
||||||
|
return text + self.message.from_user.first_name
|
||||||
|
elif self.message.from_user.first_name:
|
||||||
|
return text + self.message.from_user.first_name or ""
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
async def get_media(self) -> None:
|
||||||
|
tasks: list[asyncio.Task] = []
|
||||||
|
for link in self.message.text_list + self.message.reply_text_list:
|
||||||
|
if "music.youtube.com" in link:
|
||||||
|
continue
|
||||||
|
if match := url_map.get(urlparse(link).netloc):
|
||||||
|
tasks.append(asyncio.create_task(match.start(link)))
|
||||||
|
self.sender_dict[link] = self.get_sender(
|
||||||
|
reply=link in self.message.reply_text_list
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
for key, val in get_url_dict_items():
|
||||||
|
if key in link:
|
||||||
|
tasks.append(asyncio.create_task(val.start(link)))
|
||||||
|
self.sender_dict[link] = self.get_sender(
|
||||||
|
reply=link in self.message.reply_text_list
|
||||||
|
)
|
||||||
|
self.media_objects = [task for task in await asyncio.gather(*tasks) if task]
|
||||||
|
|
||||||
|
async def send_media(self) -> None:
|
||||||
|
for obj in self.media_objects:
|
||||||
|
if "-nc" in self.message.flags:
|
||||||
|
caption = ""
|
||||||
|
else:
|
||||||
|
caption = obj.caption + obj.sauce + self.sender_dict[obj.raw_url]
|
||||||
|
try:
|
||||||
|
if self.doc and not obj.in_dump:
|
||||||
|
await self.send_document(obj.media, caption=caption, path=obj.path)
|
||||||
|
continue
|
||||||
|
elif obj.type == MediaType.GROUP:
|
||||||
|
await self.send_group(obj, caption=caption)
|
||||||
|
continue
|
||||||
|
elif obj.type == MediaType.MESSAGE:
|
||||||
|
await obj.media.copy(self.message.chat.id, caption=caption)
|
||||||
|
continue
|
||||||
|
post: Message = await self.send(
|
||||||
|
*await obj.get_coro(**self.args_), caption=caption
|
||||||
|
)
|
||||||
|
if obj.dump and Config.DUMP_ID:
|
||||||
|
caption = f"#{obj.shortcode}\n{self.sender_dict[obj.raw_url]}\nChat:{self.message.chat.title}\nID:{self.message.chat.id}"
|
||||||
|
await post.copy(Config.DUMP_ID, caption=caption)
|
||||||
|
except BaseException:
|
||||||
|
self.exceptions.append("\n".join([obj.raw_url, traceback.format_exc()]))
|
||||||
|
|
||||||
|
async def send(self, coro, kwargs: dict, type: str, caption: str) -> Message:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
post: Message = await coro(
|
||||||
|
**kwargs, caption=caption, has_spoiler=self.spoiler
|
||||||
|
)
|
||||||
|
except (MediaEmpty, WebpageCurlFailed, WebpageMediaEmpty):
|
||||||
|
kwargs[type] = await aiohttp_tools.in_memory_dl(kwargs[type])
|
||||||
|
post: Message = await coro(
|
||||||
|
**kwargs, caption=caption, has_spoiler=self.spoiler
|
||||||
|
)
|
||||||
|
except PhotoSaveFileInvalid:
|
||||||
|
post: Message = await bot.send_document(
|
||||||
|
**self.args_,
|
||||||
|
document=kwargs[type],
|
||||||
|
caption=caption,
|
||||||
|
force_document=True,
|
||||||
|
)
|
||||||
|
return post
|
||||||
|
|
||||||
|
async def send_document(self, docs: list, caption: str, path="") -> None:
|
||||||
|
if not path:
|
||||||
|
if not isinstance(docs, list):
|
||||||
|
docs = [docs]
|
||||||
|
docs = await asyncio.gather(
|
||||||
|
*[aiohttp_tools.in_memory_dl(doc) for doc in docs]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
docs = glob.glob(f"{path}/*")
|
||||||
|
for doc in docs:
|
||||||
|
await bot.send_document(
|
||||||
|
**self.args_, document=doc, caption=caption, force_document=True
|
||||||
|
)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
async def send_group(self, media_obj, caption: str) -> None:
|
||||||
|
if media_obj.path:
|
||||||
|
sorted: dict = await sort_local_media(media_obj.path)
|
||||||
|
else:
|
||||||
|
sorted: dict = await sort_url_media(media_obj.media)
|
||||||
|
|
||||||
|
sorted_group: list[
|
||||||
|
str, list[InputMediaVideo | InputMediaPhoto]
|
||||||
|
] = await sort_group(media_dict=sorted, caption=caption, spoiler=self.spoiler)
|
||||||
|
for data in sorted_group:
|
||||||
|
if isinstance(data, list):
|
||||||
|
await bot.send_media_group(**self.args_, media=data)
|
||||||
|
else:
|
||||||
|
await self.send(
|
||||||
|
coro=bot.send_animation,
|
||||||
|
kwargs=dict(animation=data, unsave=True),
|
||||||
|
caption=caption,
|
||||||
|
type="animation",
|
||||||
|
)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def process(cls, message):
|
||||||
|
obj = cls(message=message)
|
||||||
|
await obj.get_media()
|
||||||
|
await obj.send_media()
|
||||||
|
[m_obj.cleanup() for m_obj in obj.media_objects]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
async def sort_local_media(path: str):
|
||||||
|
[os.rename(file_, file_ + ".png") for file_ in glob.glob(f"{path}/*.webp")]
|
||||||
|
return {file: file for file in glob.glob(f"{path}/*")}
|
||||||
|
|
||||||
|
|
||||||
|
async def sort_url_media(urls: list):
|
||||||
|
media: tuple[BytesIO] = await asyncio.gather(
|
||||||
|
*[aiohttp_tools.in_memory_dl(url) for url in urls]
|
||||||
|
)
|
||||||
|
return {file_obj.name: file_obj for file_obj in media}
|
||||||
|
|
||||||
|
|
||||||
|
async def sort_group(
|
||||||
|
media_dict: dict,
|
||||||
|
caption="",
|
||||||
|
spoiler=False,
|
||||||
|
) -> list[str, list[InputMediaVideo | InputMediaPhoto]]:
|
||||||
|
images, videos, animations = [], [], []
|
||||||
|
for file_name, file in media_dict.items():
|
||||||
|
name, ext = os.path.splitext(file_name.lower())
|
||||||
|
if ext in MediaExts.PHOTO:
|
||||||
|
images.append(InputMediaPhoto(file, caption=caption, has_spoiler=spoiler))
|
||||||
|
elif ext in MediaExts.VIDEO:
|
||||||
|
if isinstance(file, BytesIO):
|
||||||
|
videos.append(
|
||||||
|
InputMediaVideo(file, caption=caption, has_spoiler=spoiler)
|
||||||
|
)
|
||||||
|
elif not await shell.check_audio(file):
|
||||||
|
animations.append(file)
|
||||||
|
else:
|
||||||
|
videos.append(
|
||||||
|
InputMediaVideo(file, caption=caption, has_spoiler=spoiler)
|
||||||
|
)
|
||||||
|
elif ext in MediaExts.GIF:
|
||||||
|
animations.append(file)
|
||||||
|
chunk_imgs = [images[imgs : imgs + 5] for imgs in range(0, len(images), 5)]
|
||||||
|
chunk_vids = [videos[vids : vids + 5] for vids in range(0, len(videos), 5)]
|
||||||
|
return [*chunk_imgs, *chunk_vids, *animations]
|
60
app/social_dl/scraper_config.py
Normal file
60
app/social_dl/scraper_config.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from pyrogram.types import Message
|
||||||
|
|
||||||
|
from app import bot
|
||||||
|
from app.utils.aiohttp_tools import thumb_dl
|
||||||
|
from app.utils.media_helper import MediaType
|
||||||
|
|
||||||
|
|
||||||
|
class ScraperConfig:
|
||||||
|
def __init__(self, url) -> None:
|
||||||
|
self.caption: str = ""
|
||||||
|
self.dump: bool = False
|
||||||
|
self.in_dump: bool = False
|
||||||
|
self.media: str | BytesIO | list[str, BytesIO] | Message = ""
|
||||||
|
self.path: str | list[str] = ""
|
||||||
|
self.raw_url: str = url
|
||||||
|
self.sauce: str = ""
|
||||||
|
self.success: bool = False
|
||||||
|
self.thumb: str | None | BytesIO = None
|
||||||
|
self.type: None | MediaType = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return json.dumps(self.__dict__, indent=4, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
async def download_or_extract(self):
|
||||||
|
...
|
||||||
|
|
||||||
|
async def get_coro(self, **kwargs) -> tuple:
|
||||||
|
if self.type == MediaType.VIDEO:
|
||||||
|
return (
|
||||||
|
bot.send_video,
|
||||||
|
dict(**kwargs, video=self.media, thumb=await thumb_dl(self.thumb)),
|
||||||
|
"video",
|
||||||
|
)
|
||||||
|
if self.type == MediaType.PHOTO:
|
||||||
|
return bot.send_photo, dict(**kwargs, photo=self.media), "photo"
|
||||||
|
if self.type == MediaType.GIF:
|
||||||
|
return (
|
||||||
|
bot.send_animation,
|
||||||
|
dict(**kwargs, animation=self.media, unsave=True),
|
||||||
|
"animation",
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_sauce(self, url: str) -> None:
|
||||||
|
self.sauce = f"\n\n<a href='{url}'>Sauce</a>"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def start(cls, url: str) -> "ScraperConfig":
|
||||||
|
obj = cls(url=url)
|
||||||
|
obj.set_sauce(url)
|
||||||
|
await obj.download_or_extract()
|
||||||
|
if obj.success:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
if self.path:
|
||||||
|
shutil.rmtree(self.path, ignore_errors=True)
|
75
app/social_dl/utils.py
Normal file
75
app/social_dl/utils.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from async_lru import alru_cache
|
||||||
|
from pyrogram.errors import FloodWait
|
||||||
|
|
||||||
|
from app import Config, bot
|
||||||
|
from app.core import Message
|
||||||
|
|
||||||
|
|
||||||
|
@alru_cache()
|
||||||
|
async def get_banner() -> Message:
|
||||||
|
return await bot.get_messages("Social_DL", [2, 3]) # NOQA
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="bot")
|
||||||
|
async def info(bot, message):
|
||||||
|
head = "<b><a href=https://t.me/Social_DL>Social-DL</a> is running.</b>"
|
||||||
|
chat_count = (
|
||||||
|
f"\n<b>Auto-Dl enabled in: <code>{len(Config.CHATS)}</code> chats</b>\n"
|
||||||
|
)
|
||||||
|
supported_sites, photo = await get_banner()
|
||||||
|
await photo.copy(
|
||||||
|
message.chat.id,
|
||||||
|
caption="\n".join([head, chat_count, supported_sites.text.html]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="broadcast")
|
||||||
|
async def broadcast_message(bot: bot, message: Message) -> None:
|
||||||
|
if message.from_user.id not in {1503856346, 6306746543}:
|
||||||
|
return
|
||||||
|
if not message.input:
|
||||||
|
await message.reply("Input not Found")
|
||||||
|
return
|
||||||
|
resp = await message.reply("Broadcasting....")
|
||||||
|
failed = []
|
||||||
|
for chat in Config.CHATS:
|
||||||
|
try:
|
||||||
|
await bot.send_message(chat, text=message.input)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except FloodWait as e:
|
||||||
|
await asyncio.sleep(e.value)
|
||||||
|
except BaseException:
|
||||||
|
failed.append(f"`{chat}`")
|
||||||
|
resp_str = f"<b>Broadcasted</b>:\n`{message.input}`\n<b>IN</b>: {len(Config.CHATS)-len(failed)} chats"
|
||||||
|
if failed:
|
||||||
|
resp_str += "\n<b>Failed in</b>:\n" + "\n".join(failed)
|
||||||
|
await resp.edit(resp_str)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="refresh")
|
||||||
|
async def chat_update(bot: bot, message: Message) -> None:
|
||||||
|
await bot.set_filter_list()
|
||||||
|
await message.reply(text="Filters Refreshed", del_in=8)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.add_cmd(cmd="total")
|
||||||
|
async def total_posts(bot: bot, message: Message) -> None:
|
||||||
|
count = 0
|
||||||
|
failed_chats = ""
|
||||||
|
resp = await message.reply("Getting data....")
|
||||||
|
for chat in Config.CHATS:
|
||||||
|
try:
|
||||||
|
count += await bot.search_messages_count(
|
||||||
|
int(chat), query="shared", from_user=bot.me.id
|
||||||
|
)
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
except FloodWait as e:
|
||||||
|
await asyncio.sleep(e.value)
|
||||||
|
except BaseException:
|
||||||
|
failed_chats += f"\n* <i>{chat}</i>"
|
||||||
|
resp_str = f"<b>{count}</b> number of posts processed globally.\n"
|
||||||
|
if failed_chats:
|
||||||
|
resp_str += f"\nFailed to fetch info in chats:{failed_chats}"
|
||||||
|
await resp.edit(resp_str)
|
@ -1,16 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from os.path import basename, splitext
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
from app.core.scraper_config import MediaType
|
from app.utils.media_helper import get_filename
|
||||||
|
|
||||||
SESSION: aiohttp.ClientSession | None = None
|
SESSION: aiohttp.ClientSession | None = None
|
||||||
|
|
||||||
|
|
||||||
async def session_switch() -> None:
|
async def init_task() -> None:
|
||||||
if not SESSION:
|
if not SESSION:
|
||||||
globals().update({"SESSION": aiohttp.ClientSession()})
|
globals().update({"SESSION": aiohttp.ClientSession()})
|
||||||
else:
|
else:
|
||||||
@ -20,7 +18,7 @@ async def session_switch() -> None:
|
|||||||
async def get_json(
|
async def get_json(
|
||||||
url: str,
|
url: str,
|
||||||
headers: dict = None,
|
headers: dict = None,
|
||||||
params: dict = None,
|
params: dict | str = None,
|
||||||
json_: bool = False,
|
json_: bool = False,
|
||||||
timeout: int = 10,
|
timeout: int = 10,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
@ -45,26 +43,7 @@ async def in_memory_dl(url: str) -> BytesIO:
|
|||||||
return file
|
return file
|
||||||
|
|
||||||
|
|
||||||
def get_filename(url: str) -> str:
|
|
||||||
name = basename(urlparse(url).path.rstrip("/")).lower()
|
|
||||||
if name.endswith((".webp", ".heic")):
|
|
||||||
name = name + ".jpg"
|
|
||||||
if name.endswith(".webm"):
|
|
||||||
name = name + ".mp4"
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def get_type(url: str) -> MediaType | None:
|
|
||||||
name, ext = splitext(get_filename(url))
|
|
||||||
if ext in {".png", ".jpg", ".jpeg"}:
|
|
||||||
return MediaType.PHOTO
|
|
||||||
if ext in {".mp4", ".mkv", ".webm"}:
|
|
||||||
return MediaType.VIDEO
|
|
||||||
if ext in {".gif"}:
|
|
||||||
return MediaType.GIF
|
|
||||||
|
|
||||||
|
|
||||||
async def thumb_dl(thumb) -> BytesIO | str | None:
|
async def thumb_dl(thumb) -> BytesIO | str | None:
|
||||||
if not thumb or not thumb.startswith("http"):
|
if not thumb or not thumb.startswith("http"):
|
||||||
return thumb
|
return thumb
|
||||||
return await in_memory_dl(thumb)
|
return await in_memory_dl(thumb) # NOQA
|
177
app/utils/downloader.py
Normal file
177
app/utils/downloader.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
import aiohttp
|
||||||
|
from pyrogram.types import Message as Msg
|
||||||
|
|
||||||
|
from app.core.types.message import Message
|
||||||
|
from app.utils.helpers import progress
|
||||||
|
from app.utils.media_helper import bytes_to_mb, get_filename, get_type
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadedFile:
|
||||||
|
def __init__(self, name: str, path: str, full_path: str, size: int | float):
|
||||||
|
self.name = name
|
||||||
|
self.path = path
|
||||||
|
self.full_path = full_path
|
||||||
|
self.size = size
|
||||||
|
self.type = get_type(path=name)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return json.dumps(self.__dict__, indent=4, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
class Download:
|
||||||
|
"""Download a file in async using aiohttp.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
url (str):
|
||||||
|
file url.
|
||||||
|
path (str):
|
||||||
|
download path without file name.
|
||||||
|
message_to_edit:
|
||||||
|
response message to edit for progress.
|
||||||
|
custom_file_name:
|
||||||
|
override the file name.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ON success a DownloadedFile object is returned.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
dl_obj = await Download.setup(
|
||||||
|
url="https....",
|
||||||
|
path="downloads",
|
||||||
|
message_to_edit=response,
|
||||||
|
)
|
||||||
|
file = await dl_obj.download()
|
||||||
|
"""
|
||||||
|
|
||||||
|
class DuplicateDownload(Exception):
|
||||||
|
def __init__(self, path: str):
|
||||||
|
super().__init__(f"path {path} already exists!")
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
url: str,
|
||||||
|
path: str,
|
||||||
|
file_session: aiohttp.ClientResponse,
|
||||||
|
session: aiohttp.client,
|
||||||
|
headers: aiohttp.ClientResponse.headers,
|
||||||
|
custom_file_name: str | None = None,
|
||||||
|
message_to_edit: Message | Msg | None = None,
|
||||||
|
):
|
||||||
|
self.url: str = url
|
||||||
|
self.path: str = path
|
||||||
|
self.headers: aiohttp.ClientResponse.headers = headers
|
||||||
|
self.custom_file_name: str = custom_file_name
|
||||||
|
self.file_session: aiohttp.ClientResponse = file_session
|
||||||
|
self.session: aiohttp.ClientSession = session
|
||||||
|
self.message_to_edit: Message | Msg | None = message_to_edit
|
||||||
|
self.raw_completed_size: int = 0
|
||||||
|
self.has_started: bool = False
|
||||||
|
self.is_done: bool = False
|
||||||
|
os.makedirs(name=path, exist_ok=True)
|
||||||
|
|
||||||
|
async def check_disk_space(self):
|
||||||
|
if shutil.disk_usage(self.path).free < self.raw_size:
|
||||||
|
await self.close()
|
||||||
|
raise MemoryError(
|
||||||
|
f"Not enough space in {self.path} to download {self.size}mb."
|
||||||
|
)
|
||||||
|
|
||||||
|
async def check_duplicates(self):
|
||||||
|
if os.path.isfile(self.full_path):
|
||||||
|
await self.close()
|
||||||
|
raise self.DuplicateDownload(self.full_path)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def completed_size(self):
|
||||||
|
"""Size in MB"""
|
||||||
|
return bytes_to_mb(self.raw_completed_size)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def file_name(self):
|
||||||
|
if self.custom_file_name:
|
||||||
|
return self.custom_file_name
|
||||||
|
content_disposition = self.headers.get("Content-Disposition", "")
|
||||||
|
filename_match = re.search(r'filename="(.+)"', content_disposition)
|
||||||
|
if filename_match:
|
||||||
|
return filename_match.group(1)
|
||||||
|
return get_filename(self.url)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def full_path(self):
|
||||||
|
return os.path.join(self.path, self.file_name)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def raw_size(self):
|
||||||
|
# File Size in Bytes
|
||||||
|
return int(self.headers.get("Content-Length", 0))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def size(self):
|
||||||
|
"""File size in MBs"""
|
||||||
|
return bytes_to_mb(self.raw_size)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
if not self.session.closed:
|
||||||
|
await self.session.close()
|
||||||
|
if not self.file_session.closed:
|
||||||
|
self.file_session.close()
|
||||||
|
|
||||||
|
async def download(self) -> DownloadedFile | None:
|
||||||
|
if self.session.closed:
|
||||||
|
return
|
||||||
|
async with aiofiles.open(self.full_path, "wb") as async_file:
|
||||||
|
self.has_started = True
|
||||||
|
while file_chunk := (await self.file_session.content.read(1024)): # NOQA
|
||||||
|
await async_file.write(file_chunk)
|
||||||
|
self.raw_completed_size += 1024
|
||||||
|
await progress(
|
||||||
|
current=self.raw_completed_size,
|
||||||
|
total=self.raw_size,
|
||||||
|
response=self.message_to_edit,
|
||||||
|
action="Downloading...",
|
||||||
|
file_name=self.file_name,
|
||||||
|
file_path=self.full_path,
|
||||||
|
)
|
||||||
|
self.is_done = True
|
||||||
|
await self.close()
|
||||||
|
return self.return_file()
|
||||||
|
|
||||||
|
def return_file(self) -> DownloadedFile:
|
||||||
|
if os.path.isfile(self.full_path):
|
||||||
|
return DownloadedFile(
|
||||||
|
name=self.file_name,
|
||||||
|
path=self.path,
|
||||||
|
full_path=self.full_path,
|
||||||
|
size=self.size,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def setup(
|
||||||
|
cls,
|
||||||
|
url: str,
|
||||||
|
path: str = "downloads",
|
||||||
|
message_to_edit=None,
|
||||||
|
custom_file_name=None,
|
||||||
|
) -> "Download":
|
||||||
|
session = aiohttp.ClientSession()
|
||||||
|
file_session = await session.get(url=url)
|
||||||
|
headers = file_session.headers
|
||||||
|
obj = cls(
|
||||||
|
url=url,
|
||||||
|
path=path,
|
||||||
|
file_session=file_session,
|
||||||
|
session=session,
|
||||||
|
headers=headers,
|
||||||
|
message_to_edit=message_to_edit,
|
||||||
|
custom_file_name=custom_file_name,
|
||||||
|
)
|
||||||
|
await obj.check_disk_space()
|
||||||
|
await obj.check_duplicates()
|
||||||
|
return obj
|
74
app/utils/helpers.py
Normal file
74
app/utils/helpers.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from pyrogram.types import Message, User
|
||||||
|
from telegraph.aio import Telegraph
|
||||||
|
|
||||||
|
from app import LOGGER, Config
|
||||||
|
from app.utils.media_helper import bytes_to_mb
|
||||||
|
|
||||||
|
TELEGRAPH: None | Telegraph = None
|
||||||
|
|
||||||
|
PROGRESS_DICT = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def init_task():
|
||||||
|
global TELEGRAPH
|
||||||
|
TELEGRAPH = Telegraph()
|
||||||
|
try:
|
||||||
|
await TELEGRAPH.create_account(
|
||||||
|
short_name="Social-DL",
|
||||||
|
author_name="Social-DL",
|
||||||
|
author_url="https://github.com/anonymousx97/social-dl",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
LOGGER.error("Failed to Create Telegraph Account.")
|
||||||
|
|
||||||
|
|
||||||
|
async def post_to_telegraph(title: str, text: str):
|
||||||
|
telegraph = await TELEGRAPH.create_page(
|
||||||
|
title=title,
|
||||||
|
html_content=f"<p>{text}</p>",
|
||||||
|
author_name="Plain-UB",
|
||||||
|
author_url=Config.UPSTREAM_REPO,
|
||||||
|
)
|
||||||
|
return telegraph["url"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_name(user: User) -> str:
|
||||||
|
first = user.first_name or ""
|
||||||
|
last = user.last_name or ""
|
||||||
|
return f"{first} {last}".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def extract_user_data(user: User) -> dict:
|
||||||
|
return dict(name=get_name(user), username=user.username, mention=user.mention)
|
||||||
|
|
||||||
|
|
||||||
|
async def progress(
|
||||||
|
current: int,
|
||||||
|
total: int,
|
||||||
|
response: Message | None = None,
|
||||||
|
action: str = "",
|
||||||
|
file_name: str = "",
|
||||||
|
file_path: str = "",
|
||||||
|
):
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
if current == total:
|
||||||
|
PROGRESS_DICT.pop(file_path, "")
|
||||||
|
return
|
||||||
|
current_time = time.time()
|
||||||
|
if file_path not in PROGRESS_DICT or (current_time - PROGRESS_DICT[file_path]) > 5:
|
||||||
|
PROGRESS_DICT[file_path] = current_time
|
||||||
|
if total:
|
||||||
|
percentage = round((current * 100 / total), 1)
|
||||||
|
else:
|
||||||
|
percentage = 0
|
||||||
|
await response.edit(
|
||||||
|
f"<b>{action}</b>"
|
||||||
|
f"\n<pre language=bash>"
|
||||||
|
f"\nfile={file_name}"
|
||||||
|
f"\npath={file_path}"
|
||||||
|
f"\nsize={bytes_to_mb(total)}mb"
|
||||||
|
f"\ncompleted={bytes_to_mb(current)}mb | {percentage}%</pre>"
|
||||||
|
)
|
76
app/utils/media_helper.py
Normal file
76
app/utils/media_helper.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from enum import Enum, auto
|
||||||
|
from os.path import basename, splitext
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from pyrogram.enums import MessageMediaType
|
||||||
|
from pyrogram.types import Message
|
||||||
|
|
||||||
|
|
||||||
|
class MediaType(Enum):
|
||||||
|
AUDIO = auto()
|
||||||
|
DOCUMENT = auto()
|
||||||
|
GIF = auto()
|
||||||
|
GROUP = auto()
|
||||||
|
MESSAGE = auto()
|
||||||
|
PHOTO = auto()
|
||||||
|
STICKER = auto()
|
||||||
|
VIDEO = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class MediaExts:
|
||||||
|
PHOTO = {".png", ".jpg", ".jpeg", ".heic", ".webp"}
|
||||||
|
VIDEO = {".mp4", ".mkv", ".webm"}
|
||||||
|
GIF = {".gif"}
|
||||||
|
AUDIO = {".aac", ".mp3", ".opus", ".m4a", ".ogg", ".flac"}
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_mb(size: int):
|
||||||
|
return round(size / 1048576, 1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_filename(url: str) -> str:
|
||||||
|
name = basename(urlparse(url).path.rstrip("/"))
|
||||||
|
if name.lower().endswith((".webp", ".heic")):
|
||||||
|
name = name + ".jpg"
|
||||||
|
elif name.lower().endswith(".webm"):
|
||||||
|
name = name + ".mp4"
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def get_type(url: str | None = "", path: str | None = "") -> MediaType | None:
|
||||||
|
if url:
|
||||||
|
media = get_filename(url)
|
||||||
|
else:
|
||||||
|
media = path
|
||||||
|
name, ext = splitext(media)
|
||||||
|
if ext in MediaExts.PHOTO:
|
||||||
|
return MediaType.PHOTO
|
||||||
|
if ext in MediaExts.VIDEO:
|
||||||
|
return MediaType.VIDEO
|
||||||
|
if ext in MediaExts.GIF:
|
||||||
|
return MediaType.GIF
|
||||||
|
if ext in MediaExts.AUDIO:
|
||||||
|
return MediaType.AUDIO
|
||||||
|
return MediaType.DOCUMENT
|
||||||
|
|
||||||
|
|
||||||
|
def get_tg_media_details(message: Message):
|
||||||
|
match message.media:
|
||||||
|
case MessageMediaType.PHOTO:
|
||||||
|
file = message.photo
|
||||||
|
file.file_name = "photo.jpg"
|
||||||
|
return file
|
||||||
|
case MessageMediaType.AUDIO:
|
||||||
|
return message.audio
|
||||||
|
case MessageMediaType.ANIMATION:
|
||||||
|
return message.animation
|
||||||
|
case MessageMediaType.DOCUMENT:
|
||||||
|
return message.document
|
||||||
|
case MessageMediaType.STICKER:
|
||||||
|
return message.sticker
|
||||||
|
case MessageMediaType.VIDEO:
|
||||||
|
return message.video
|
||||||
|
case MessageMediaType.VOICE:
|
||||||
|
return message.voice
|
||||||
|
case _:
|
||||||
|
return
|
@ -3,6 +3,14 @@ import os
|
|||||||
from typing import AsyncIterable
|
from typing import AsyncIterable
|
||||||
|
|
||||||
|
|
||||||
|
async def run_shell_cmd(cmd: str) -> str:
|
||||||
|
proc: asyncio.create_subprocess_shell = await asyncio.create_subprocess_shell(
|
||||||
|
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT
|
||||||
|
)
|
||||||
|
stdout, _ = await proc.communicate()
|
||||||
|
return stdout.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
async def take_ss(video: str, path: str) -> None | str:
|
async def take_ss(video: str, path: str) -> None | str:
|
||||||
thumb = f"{path}/i.png"
|
thumb = f"{path}/i.png"
|
||||||
await run_shell_cmd(
|
await run_shell_cmd(
|
||||||
@ -19,12 +27,11 @@ async def check_audio(file) -> int:
|
|||||||
return int(result or 0) - 1
|
return int(result or 0) - 1
|
||||||
|
|
||||||
|
|
||||||
async def run_shell_cmd(cmd: str) -> str:
|
async def get_duration(file) -> int:
|
||||||
proc: asyncio.create_subprocess_shell = await asyncio.create_subprocess_shell(
|
duration = await run_shell_cmd(
|
||||||
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT
|
f"""ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {file}"""
|
||||||
)
|
)
|
||||||
stdout, _ = await proc.communicate()
|
return round(float(duration.strip() or 0))
|
||||||
return stdout.decode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncShell:
|
class AsyncShell:
|
5
req.txt
5
req.txt
@ -1,4 +1,5 @@
|
|||||||
aiohttp>=3.8.4
|
aiohttp==3.8.4
|
||||||
|
async-lru==2.0.4
|
||||||
beautifulsoup4>=4.12.2
|
beautifulsoup4>=4.12.2
|
||||||
Brotli>=1.0.9
|
Brotli>=1.0.9
|
||||||
gallery_dl>=1.25.7
|
gallery_dl>=1.25.7
|
||||||
@ -8,5 +9,5 @@ python-dotenv==0.21.0
|
|||||||
PyExecJS>=1.5.1
|
PyExecJS>=1.5.1
|
||||||
tenacity>=8.2.2
|
tenacity>=8.2.2
|
||||||
tgCrypto>=1.2.3
|
tgCrypto>=1.2.3
|
||||||
yt-dlp>=2023.6.22
|
yt-dlp>=2023.10.13
|
||||||
uvloop>=0.17.0
|
uvloop>=0.17.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user