diff --git a/app/__init__.py b/app/__init__.py index f10dcbd..4f8d7fb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,20 +1,11 @@ -import os -import tracemalloc - -from dotenv import load_dotenv - -tracemalloc.start() - -load_dotenv("config.env") - - -if "com.termux" not in os.environ.get("PATH", ""): - import uvloop - - uvloop.install() - - -from app.config import Config # NOQA -from app.core import DB, DB_CLIENT, CustomDB, Message, Convo # NOQA -from app.core.client import BOT, bot # NOQA -from app.core.logger import LOGGER # NOQA +from ub_core import ( + BOT, + DB, + DB_CLIENT, + LOGGER, + Config, + Convo, + CustomDB, + Message, + bot, +) diff --git a/app/config.py b/app/config.py deleted file mode 100644 index 2241e7c..0000000 --- a/app/config.py +++ /dev/null @@ -1,72 +0,0 @@ -import asyncio -from os import environ, path -from typing import Callable, Coroutine - -from git import Repo - -from app.utils import Str - - -class Cmd(Str): - def __init__(self, cmd: str, func: Callable, cmd_path: str, sudo: bool): - self.cmd: str = cmd - self.cmd_path: str = cmd_path - self.dirname: str = path.basename(path.dirname(cmd_path)) - self.doc: str = func.__doc__ or "Not Documented." - self.func: Callable = func - self.loaded = False - self.sudo: bool = sudo - - -class Config: - BOT_NAME = "PLAIN-UB" - - CMD = Cmd - - CMD_DICT: dict[str, Cmd] = {} - - CMD_TRIGGER: str = environ.get("CMD_TRIGGER", ".") - - DEV_MODE: int = int(environ.get("DEV_MODE", 0)) - - DISABLED_SUPERUSERS: list[int] = [] - - FBAN_LOG_CHANNEL: int = int( - environ.get("FBAN_LOG_CHANNEL", environ.get("LOG_CHAT")) - ) - - FBAN_SUDO_ID: int = int(environ.get("FBAN_SUDO_ID", 0)) - - FBAN_SUDO_TRIGGER: str = environ.get("FBAN_SUDO_TRIGGER") - - GEMINI_API_KEY: str = environ.get("GEMINI_API_KEY") - - INIT_TASKS: list[Coroutine] = [] - - LOG_CHAT: int = int(environ.get("LOG_CHAT")) - - MESSAGE_LOGGER_CHAT: int = int(environ.get("MESSAGE_LOGGER_CHAT", LOG_CHAT)) - - MESSAGE_LOGGER_TASK: asyncio.Task | None = None - - OWNER_ID: int = int(environ.get("OWNER_ID")) - - PM_GUARD: bool = False - - PM_LOGGER: bool = False - - REPO: Repo = Repo(".") - - SUDO: bool = False - - SUDO_TRIGGER: str = environ.get("SUDO_TRIGGER", "!") - - SUDO_USERS: list[int] = [] - - SUPERUSERS: list[int] = [] - - TAG_LOGGER: bool = False - - UPSTREAM_REPO: str = environ.get( - "UPSTREAM_REPO", "https://github.com/thedragonsinn/plain-ub" - ) diff --git a/app/core/__init__.py b/app/core/__init__.py deleted file mode 100644 index 0309fce..0000000 --- a/app/core/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from app.core.conversation import Conversation as Convo -from app.core.db import DB, DB_CLIENT, CustomDB -from app.core.types.message import Message diff --git a/app/core/client.py b/app/core/client.py deleted file mode 100644 index a9c2160..0000000 --- a/app/core/client.py +++ /dev/null @@ -1,76 +0,0 @@ -import asyncio -import glob -import importlib -import logging -import os -import sys - -from pyrogram import Client, idle -from pyrogram.enums import ParseMode - -from app import DB_CLIENT, Config -from app.core.conversation import Conversation -from app.core.decorators.add_cmd import AddCmd -from app.core.methods import ChannelLogger, SendMessage -from app.utils.aiohttp_tools import aio - -LOGGER = logging.getLogger(Config.BOT_NAME) - - -def import_modules(): - for py_module in glob.glob(pathname="app/**/[!^_]*.py", recursive=True): - name = os.path.splitext(py_module)[0] - py_name = name.replace("/", ".") - try: - mod = importlib.import_module(py_name) - if hasattr(mod, "init_task"): - Config.INIT_TASKS.append(mod.init_task()) - except Exception as ie: - LOGGER.error(ie, exc_info=True) - - -class BOT(AddCmd, SendMessage, ChannelLogger, Client): - def __init__(self): - super().__init__( - name="bot", - api_id=int(os.environ.get("API_ID")), - api_hash=os.environ.get("API_HASH"), - session_string=os.environ.get("SESSION_STRING").strip(), - parse_mode=ParseMode.DEFAULT, - sleep_threshold=30, - max_concurrent_transmissions=2, - ) - self.log = LOGGER - self.Convo = Conversation - - async def boot(self) -> None: - await super().start() - LOGGER.info("Connected to TG.") - import_modules() - LOGGER.info("Plugins Imported.") - await asyncio.gather(*Config.INIT_TASKS) - Config.INIT_TASKS.clear() - LOGGER.info("Init Tasks Completed.") - await self.log_text(text="Started") - LOGGER.info("Idling...") - await idle() - await self.shut_down() - - @staticmethod - async def shut_down(): - await aio.close() - if Config.MESSAGE_LOGGER_TASK and not Config.MESSAGE_LOGGER_TASK.done(): - Config.MESSAGE_LOGGER_TASK.cancel() - LOGGER.info("DB Closed.") - DB_CLIENT.close() - - async def restart(self, hard=False) -> None: - await self.shut_down() - await super().stop(block=False) - if hard: - os.execl("/bin/bash", "/bin/bash", "run") - LOGGER.info("Restarting...") - os.execl(sys.executable, sys.executable, "-m", "app") - - -bot: BOT = BOT() diff --git a/app/core/conversation.py b/app/core/conversation.py deleted file mode 100644 index 171030a..0000000 --- a/app/core/conversation.py +++ /dev/null @@ -1,129 +0,0 @@ -import asyncio -from collections import defaultdict -from typing import Self - -from pyrogram import Client -from pyrogram.filters import Filter -from pyrogram.types import Message - -from app.utils import Str - - -class Conversation(Str): - CONVO_DICT: dict[int, list["Conversation"]] = defaultdict(list) - - class DuplicateConvo(Exception): - def __init__(self, chat: str | int): - super().__init__(f"Conversation already started with {chat} ") - - def __init__( - self, - client: Client, - chat_id: int | str, - check_for_duplicates: bool = True, - filters: Filter | None = None, - timeout: int = 10, - ): - self.chat_id: int | str = chat_id - self._client: Client = client - self.check_for_duplicates: bool = check_for_duplicates - self.filters: Filter = filters - self.response_future: asyncio.Future | None = None - self.responses: list[Message] = [] - self.timeout: int = timeout - self.set_future() - - async def __aenter__(self) -> Self: - """ - Convert Username to ID if chat_id is username. - Check Convo Dict for duplicate Convo with same ID. - Initialize Context Manager and return the Object. - """ - if isinstance(self.chat_id, str): - self.chat_id = (await self._client.get_chat(self.chat_id)).id - if self.check_for_duplicates and self.chat_id in Conversation.CONVO_DICT.keys(): - raise self.DuplicateConvo(self.chat_id) - Conversation.CONVO_DICT[self.chat_id].append(self) - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Exit Context Manager and remove Chat ID from Dict.""" - Conversation.CONVO_DICT[self.chat_id].remove(self) - if not self.response_future.done(): - self.response_future.cancel() - if not Conversation.CONVO_DICT[self.chat_id]: - Conversation.CONVO_DICT.pop(self.chat_id) - - @classmethod - async def get_resp(cls, client, *args, **kwargs) -> Message | None: - """ - Bound Method to Gracefully handle Timeout. - but only returns first Message. - """ - try: - async with cls(*args, client=client, **kwargs) as convo: - response: Message | None = await convo.get_response() - return response - except TimeoutError: - return - - def set_future(self, *args, **kwargs): - future = asyncio.Future() - future.add_done_callback(self.set_future) - self.response_future = future - - """Methods""" - - async def get_response(self, timeout: int = 0) -> Message | None: - """Returns Latest Message for Specified Filters.""" - try: - response: asyncio.Future.result = await asyncio.wait_for( - fut=self.response_future, timeout=timeout or self.timeout - ) - return response - except asyncio.TimeoutError: - raise TimeoutError("Conversation Timeout") - - async def send_message( - self, - text: str, - timeout: int = 0, - get_response: bool = False, - **kwargs, - ) -> Message | tuple[Message, Message]: - """ - Bound Method to Send Texts in Convo Chat. - Returns Sent Message and Response if get_response is True. - """ - message = await self._client.send_message( - chat_id=self.chat_id, text=text, **kwargs - ) - if get_response: - response = await self.get_response(timeout=timeout) - return message, response - return message - - async def send_document( - self, - document, - caption: str = "", - timeout: int = 0, - get_response: bool = False, - force_document: bool = True, - **kwargs, - ) -> Message | tuple[Message, Message]: - """ - Bound Method to Send Documents in Convo Chat. - Returns Sent Message and Response if get_response is True. - """ - message = await self._client.send_document( - chat_id=self.chat_id, - document=document, - caption=caption, - force_document=force_document, - **kwargs, - ) - if get_response: - response = await self.get_response(timeout=timeout) - return message, response - return message diff --git a/app/core/db.py b/app/core/db.py deleted file mode 100644 index 3539cb8..0000000 --- a/app/core/db.py +++ /dev/null @@ -1,40 +0,0 @@ -import os - -import dns.resolver -from motor.core import AgnosticClient, AgnosticDatabase -from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection - -from app.utils import Str - -dns.resolver.default_resolver = dns.resolver.Resolver(configure=False) -dns.resolver.default_resolver.nameservers = ["8.8.8.8"] - - -DB_CLIENT: AgnosticClient = AsyncIOMotorClient(os.environ.get("DB_URL").strip()) -DB: AgnosticDatabase = DB_CLIENT["plain_ub"] - - -class CustomDB(AsyncIOMotorCollection, Str): - def __init__(self, collection_name: str): - super().__init__(database=DB, name=collection_name) - - async def add_data(self, data: dict) -> None: - """ - :param data: {"_id": db_id, rest of the data} - entry is added or updated if exists. - """ - found = await self.find_one({"_id": data["_id"]}) - if not found: - await self.insert_one(data) - else: - await self.update_one({"_id": data.pop("_id")}, {"$set": data}) - - async def delete_data(self, id: int | str) -> bool | None: - """ - :param id: the db id key to delete. - :return: True if entry was deleted. - """ - found = await self.find_one({"_id": id}) - if found: - await self.delete_one({"_id": id}) - return True diff --git a/app/core/decorators/add_cmd.py b/app/core/decorators/add_cmd.py deleted file mode 100644 index 07960c6..0000000 --- a/app/core/decorators/add_cmd.py +++ /dev/null @@ -1,29 +0,0 @@ -import inspect -from functools import wraps -from typing import Callable - -from app import Config - - -class AddCmd: - @staticmethod - def add_cmd(cmd: str | list[str], allow_sudo: bool = True): - def the_decorator(func: Callable): - path = inspect.stack()[1][1] - - @wraps(func) - def wrapper(): - if isinstance(cmd, list): - for _cmd in cmd: - Config.CMD_DICT[_cmd] = Config.CMD( - cmd=_cmd, func=func, cmd_path=path, sudo=allow_sudo - ) - else: - Config.CMD_DICT[cmd] = Config.CMD( - cmd=cmd, func=func, cmd_path=path, sudo=allow_sudo - ) - - wrapper() - return func - - return the_decorator diff --git a/app/core/handlers/filters.py b/app/core/handlers/filters.py deleted file mode 100644 index d3b1b56..0000000 --- a/app/core/handlers/filters.py +++ /dev/null @@ -1,69 +0,0 @@ -from pyrogram import filters as _filters -from pyrogram.types import Message - -from app import Config -from app.core.conversation import Conversation - -convo_filter = _filters.create( - lambda _, __, message: (message.chat.id in Conversation.CONVO_DICT.keys()) - and (not message.reactions) -) - - -def cmd_check(message: Message, trigger: str, sudo: bool = False) -> bool: - start_str = message.text.split(maxsplit=1)[0] - cmd = start_str.replace(trigger, "", 1) - cmd_obj = Config.CMD_DICT.get(cmd) - if not cmd_obj: - return False - if sudo: - in_loaded = cmd_obj.loaded - has_access = cmd_obj.sudo - return in_loaded and has_access - return True - - -def basic_check(message: Message): - return message.reactions or not message.text or not message.from_user - - -def owner_check(filters, client, message: Message) -> bool: - if ( - basic_check(message) - or not message.text.startswith(Config.CMD_TRIGGER) - or message.from_user.id != Config.OWNER_ID - or (message.chat.id != Config.OWNER_ID and not message.outgoing) - ): - return False - return cmd_check(message, Config.CMD_TRIGGER) - - -owner_filter = _filters.create(owner_check) - - -def sudo_check(filters, client, message: Message) -> bool: - if ( - not Config.SUDO - or basic_check(message) - or not message.text.startswith(Config.SUDO_TRIGGER) - or message.from_user.id not in Config.SUDO_USERS - ): - return False - return cmd_check(message, Config.SUDO_TRIGGER, sudo=True) - - -sudo_filter = _filters.create(sudo_check) - - -def super_user_check(filter, client, message: Message): - if ( - basic_check(message) - or not message.text.startswith(Config.SUDO_TRIGGER) - or message.from_user.id not in Config.SUPERUSERS - or message.from_user.id in Config.DISABLED_SUPERUSERS - ): - return False - return cmd_check(message, Config.SUDO_TRIGGER) - - -super_user_filter = _filters.create(super_user_check) diff --git a/app/core/handlers/handler.py b/app/core/handlers/handler.py deleted file mode 100644 index 94cb69b..0000000 --- a/app/core/handlers/handler.py +++ /dev/null @@ -1,43 +0,0 @@ -import asyncio -from typing import Callable - -from pyrogram import StopPropagation -from pyrogram.types import Message as Msg - -from app import BOT, Config, Convo, Message, bot -from app.core.handlers import filters - - -@bot.on_message( - filters.owner_filter | filters.super_user_filter | filters.sudo_filter, group=1 -) -@bot.on_edited_message( - filters.owner_filter | filters.super_user_filter | filters.sudo_filter, group=1 -) -async def cmd_dispatcher(bot: BOT, message: Message, func: Callable = None) -> None: - message = Message.parse(message) - func = func or Config.CMD_DICT[message.cmd].func - task = asyncio.create_task(func(bot, message), name=message.task_id) - try: - await task - if message.is_from_owner: - await message.delete() - except asyncio.exceptions.CancelledError: - await bot.log_text(text=f"#Cancelled:\n{message.text}") - except StopPropagation: - raise StopPropagation - except Exception as e: - bot.log.error(e, exc_info=True, extra={"tg_message": message}) - message.stop_propagation() - - -@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_objects: list[Convo] = Convo.CONVO_DICT[message.chat.id] - for conv_object in conv_objects: - if conv_object.filters and not (await conv_object.filters(bot, message)): - continue - conv_object.responses.append(message) - conv_object.response_future.set_result(message) - message.continue_propagation() diff --git a/app/core/logger.py b/app/core/logger.py deleted file mode 100644 index 5a3dac3..0000000 --- a/app/core/logger.py +++ /dev/null @@ -1,69 +0,0 @@ -import asyncio -import os -from logging import ( - ERROR, - INFO, - WARNING, - Handler, - StreamHandler, - basicConfig, - getLogger, - handlers, -) - -from app import Config, bot - -os.makedirs(name="logs", exist_ok=True) - -LOGGER = getLogger(Config.BOT_NAME) - - -class TgErrorHandler(Handler): - def emit(self, log_record): - if not bot.is_connected: - return - self.format(log_record) - chat = "" - if hasattr(log_record, "tg_message"): - chat = ( - log_record.tg_message.chat.title - or log_record.tg_message.chat.first_name - ) - text = ( - f"#{log_record.levelname} #TRACEBACK" - f"\nChat: {chat}" - f"\nLine No: {log_record.lineno}" - f"\nFunc: {log_record.funcName}" - f"\nModule: {log_record.module}" - f"\nTime: {log_record.asctime}" - f"\nError Message:\n
{log_record.exc_text or log_record.message}
" - ) - asyncio.run_coroutine_threadsafe( - coro=bot.log_text(text=text, name="traceback.txt"), loop=bot.loop - ) - - -custom_handler = TgErrorHandler() -custom_handler.setLevel(ERROR) - -basicConfig( - level=INFO, - format="[%(levelname)s] [%(asctime)s] [%(name)s] [%(module)s]: %(message)s", - datefmt="%d-%m-%y %I:%M:%S %p", - handlers={ - handlers.RotatingFileHandler( - filename="logs/app_logs.txt", - mode="a", - maxBytes=5 * 1024 * 1024, - backupCount=2, - encoding=None, - delay=False, - ), - StreamHandler(), - custom_handler, - }, -) - -getLogger("pyrogram").setLevel(WARNING) -getLogger("httpx").setLevel(WARNING) -getLogger("aiohttp.access").setLevel(WARNING) diff --git a/app/core/methods/__init__.py b/app/core/methods/__init__.py deleted file mode 100644 index 2bb068b..0000000 --- a/app/core/methods/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from app.core.methods.channel_loggers import ChannelLogger -from app.core.methods.send_message import SendMessage diff --git a/app/core/methods/channel_loggers.py b/app/core/methods/channel_loggers.py deleted file mode 100644 index d423c37..0000000 --- a/app/core/methods/channel_loggers.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging - -from pyrogram import Client -from pyrogram.enums import ParseMode - -from app import Config -from app.core.types.message import Message - -LOGGER = logging.getLogger(Config.BOT_NAME) - - -class ChannelLogger(Client): - async def log_text( - self, - text, - name="log.txt", - disable_web_page_preview=True, - parse_mode=ParseMode.HTML, - type: str = "", - ) -> Message: - if type: - if hasattr(LOGGER, type): - getattr(LOGGER, type)(text) - text = f"#{type.upper()}\n{text}" - - return (await self.send_message( - chat_id=Config.LOG_CHAT, - text=text, - name=name, # NOQA - disable_web_page_preview=disable_web_page_preview, - parse_mode=parse_mode, - )) # fmt:skip - - @staticmethod - async def log_message(message: Message): - return (await message.copy(chat_id=Config.LOG_CHAT)) # fmt: skip diff --git a/app/core/methods/send_message.py b/app/core/methods/send_message.py deleted file mode 100644 index c002b9f..0000000 --- a/app/core/methods/send_message.py +++ /dev/null @@ -1,31 +0,0 @@ -from io import BytesIO - -from pyrogram import Client - -from app.core.types.message import Message - - -class SendMessage(Client): - async def send_message( - self, - chat_id: int | str, - text, - name: str = "output.txt", - disable_web_page_preview: bool = False, - **kwargs, - ) -> Message: - if not isinstance(text, str): - text = str(text) - if len(text) < 4096: - message = await super().send_message( - chat_id=chat_id, - text=text, - disable_web_page_preview=disable_web_page_preview, - **kwargs, - ) - return Message.parse(message=message) - doc = BytesIO(bytes(text, encoding="utf-8")) - doc.name = name - return (await super().send_document( - chat_id=chat_id, document=doc, **kwargs - )) # fmt: skip diff --git a/app/core/types/message.py b/app/core/types/message.py deleted file mode 100644 index 6e2084a..0000000 --- a/app/core/types/message.py +++ /dev/null @@ -1,176 +0,0 @@ -import asyncio -from functools import cached_property -from typing import Self - -from pyrogram.enums import MessageEntityType -from pyrogram.errors import MessageDeleteForbidden -from pyrogram.filters import Filter -from pyrogram.types import Message as Msg -from pyrogram.types import User - -from app import Config -from app.core.conversation import Conversation - - -class Message(Msg): - def __init__(self, message: Msg | Self) -> None: - kwargs = self.sanitize_message(message) - super().__init__(**kwargs) - - @cached_property - def cmd(self) -> str | None: - if not self.text_list: - return - raw_cmd = self.text_list[0] - cmd = raw_cmd.replace(self.trigger, "", 1) - return cmd if cmd in Config.CMD_DICT.keys() else None - - @cached_property - def flags(self) -> list: - return [i for i in self.text_list if i.startswith("-")] - - @cached_property - def filtered_input(self) -> str: - split_lines = self.input.split(sep="\n", maxsplit=1) - split_lines[0] = " ".join( - [word for word in split_lines[0].split(" ") if word not in self.flags] - ) - return "\n".join(split_lines) - - @cached_property - def input(self) -> str: - if len(self.text_list) > 1: - return self.text.split(maxsplit=1)[-1] - return "" - - @cached_property - def is_from_owner(self) -> bool: - return self.from_user and self.from_user.id == Config.OWNER_ID - - @cached_property - def replied(self) -> "Message": - if self.reply_to_message: - return Message.parse(self.reply_to_message) - - @cached_property - def reply_id(self) -> int | None: - return self.replied.id if self.replied else None - - @cached_property - def replied_task_id(self) -> str | None: - return self.replied.task_id if self.replied else None - - @cached_property - def reply_text_list(self) -> list: - return self.replied.text_list if self.replied else [] - - @cached_property - def task_id(self) -> str: - return f"{self.chat.id}-{self.id}" - - @cached_property - def text_list(self) -> list: - return self.text.split() if self.text else [] - - @cached_property - def trigger(self): - return Config.CMD_TRIGGER if self.is_from_owner else Config.SUDO_TRIGGER - - async def async_deleter(self, del_in, task, block) -> None: - if block: - x = await task - await asyncio.sleep(del_in) - await x.delete() - return x - else: - asyncio.create_task( - self.async_deleter(del_in=del_in, task=task, block=True) - ) - - async def delete(self, reply: bool = False) -> None: - try: - await super().delete() - if reply and self.replied: - await self.replied.delete() - except MessageDeleteForbidden: - pass - - async def edit( - self, text, del_in: int = 0, block=True, name: str = "output.txt", **kwargs - ) -> "Message": - if len(str(text)) < 4096: - task = super().edit_text(text=text, **kwargs) - if del_in: - reply = await self.async_deleter(task=task, del_in=del_in, block=block) - else: - reply = Message.parse((await task)) # fmt:skip - self.text = reply.text - else: - _, reply = await asyncio.gather( - super().delete(), self.reply(text, name=name, **kwargs) - ) - return reply - - async def extract_user_n_reason(self) -> tuple[User | str | Exception, str | None]: - if self.replied: - return self.replied.from_user, self.filtered_input - input_text_list = self.filtered_input.split(maxsplit=1) - if not input_text_list: - return ( - "Unable to Extract User info.\nReply to a user or input @ | id.", - None, - ) - user = input_text_list[0] - reason = None - if len(input_text_list) >= 2: - reason = input_text_list[1] - if self.entities: - for entity in self.entities: - if entity == MessageEntityType.MENTION: - return entity.user, reason - if user.isdigit(): - user = int(user) - elif user.startswith("@"): - user = user.strip("@") - try: - return (await self._client.get_users(user_ids=user)), reason - except Exception: - return user, reason - - async def get_response(self, filters: Filter = None, timeout: int = 8): - response: Message | None = await Conversation.get_resp( - client=self._client, chat_id=self.chat.id, filters=filters, timeout=timeout - ) - return response - - async def log(self): - return (await self.copy(Config.LOG_CHAT)) # fmt:skip - - async def reply( - self, text, del_in: int = 0, block: bool = True, **kwargs - ) -> "Message": - task = self._client.send_message( - chat_id=self.chat.id, text=text, reply_to_message_id=self.id, **kwargs - ) - if del_in: - await self.async_deleter(task=task, del_in=del_in, block=block) - else: - return Message.parse((await task)) # fmt:skip - - @staticmethod - def sanitize_message(message): - kwargs = vars(message).copy() - kwargs["client"] = kwargs.pop("_client", message._client) - [ - kwargs.pop(arg, 0) - for arg in dir(Message) - if ( - isinstance(getattr(Message, arg, 0), (cached_property, property)) - and not hasattr(Msg, arg) - ) - ] - return kwargs - - @classmethod - def parse(cls, message: Msg) -> "Message": - return cls(message) diff --git a/app/extra_config.py b/app/extra_config.py new file mode 100644 index 0000000..0b09aa4 --- /dev/null +++ b/app/extra_config.py @@ -0,0 +1,29 @@ +from os import environ + +BOT_NAME = "PLAIN-UB" + +DISABLED_SUPERUSERS: list[int] = [] + +FBAN_LOG_CHANNEL: int = int(environ.get("FBAN_LOG_CHANNEL", environ.get("LOG_CHAT"))) + +FBAN_SUDO_ID: int = int(environ.get("FBAN_SUDO_ID", 0)) + +FBAN_SUDO_TRIGGER: str = environ.get("FBAN_SUDO_TRIGGER") + +GEMINI_API_KEY: str = environ.get("GEMINI_API_KEY") + +LOAD_HANDLERS: bool = True + +MESSAGE_LOGGER_CHAT: int = int( + environ.get("MESSAGE_LOGGER_CHAT", environ.get("LOG_CHAT")) +) + +PM_GUARD: bool = False + +PM_LOGGER: bool = False + +TAG_LOGGER: bool = False + +UPSTREAM_REPO: str = environ.get( + "UPSTREAM_REPO", "https://github.com/thedragonsinn/plain-ub" +) diff --git a/app/plugins/admin/fbans.py b/app/plugins/admin/fbans.py index cc6d56e..e7c029e 100644 --- a/app/plugins/admin/fbans.py +++ b/app/plugins/admin/fbans.py @@ -3,9 +3,9 @@ import asyncio from pyrogram import filters from pyrogram.enums import ChatMemberStatus, ChatType from pyrogram.types import Chat, User +from ub_core.utils.helpers import get_name -from app import BOT, Config, CustomDB, Message, bot -from app.utils.helpers import get_name +from app import BOT, Config, CustomDB, Message, bot, extra_config FED_DB = CustomDB("FED_LIST") @@ -116,7 +116,7 @@ async def fed_ban(bot: BOT, message: Message): if not message.replied: await progress.edit("Reply to a proof") return - proof = await message.replied.forward(Config.FBAN_LOG_CHANNEL) + proof = await message.replied.forward(extra_config.FBAN_LOG_CHANNEL) proof_str = f"\n{ {proof.link} }" reason = f"{reason}{proof_str}" @@ -229,7 +229,9 @@ async def perform_fed_task( if not message.is_from_owner: resp_str += f"\n\nBy: {get_name(message.from_user)}" await bot.send_message( - chat_id=Config.FBAN_LOG_CHANNEL, text=resp_str, disable_web_page_preview=True + chat_id=extra_config.FBAN_LOG_CHANNEL, + text=resp_str, + disable_web_page_preview=True, ) await progress.edit( text=resp_str, del_in=5, block=True, disable_web_page_preview=True @@ -238,9 +240,9 @@ async def perform_fed_task( async def handle_sudo_fban(command: str): - if not (Config.FBAN_SUDO_ID and Config.FBAN_SUDO_TRIGGER): + if not (extra_config.FBAN_SUDO_ID and extra_config.FBAN_SUDO_TRIGGER): return - sudo_cmd = command.replace("/", Config.FBAN_SUDO_TRIGGER, 1) + sudo_cmd = command.replace("/", extra_config.FBAN_SUDO_TRIGGER, 1) await bot.send_message( - chat_id=Config.FBAN_SUDO_ID, text=sudo_cmd, disable_web_page_preview=True + chat_id=extra_config.FBAN_SUDO_ID, text=sudo_cmd, disable_web_page_preview=True ) diff --git a/app/plugins/dev/exec.py b/app/plugins/dev/exec.py deleted file mode 100644 index 6ae0b0b..0000000 --- a/app/plugins/dev/exec.py +++ /dev/null @@ -1,66 +0,0 @@ -import asyncio -import inspect -import sys -import traceback -from io import StringIO - -from pyrogram.enums import ParseMode - -from app import Config, bot, BOT, Message, CustomDB, DB, DB_CLIENT # isort:skip -from app.utils import shell # isort:skip -from app.utils.aiohttp_tools import aio # isort:skip - - -async def executor(bot: BOT, message: Message) -> Message | None: - """ - CMD: PY - INFO: Run Python Code. - FLAGS: -s to only show output. - USAGE: - .py [-s] return 1 - """ - code: str = message.filtered_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 Exception: - 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">> ```\n{output}```" - else: - output = f"```python\n{code}```\n\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( - cmd="py", - func=executor, - cmd_path=inspect.stack()[0][1], - sudo=False, - ) diff --git a/app/plugins/dev/loader.py b/app/plugins/dev/loader.py deleted file mode 100644 index cb402b5..0000000 --- a/app/plugins/dev/loader.py +++ /dev/null @@ -1,46 +0,0 @@ -import importlib -import inspect -import os -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") - ) and "-r" not in message.flags: - await message.reply("Reply to a Plugin.") - return - if "-r" in message.flags: - plugin = message.filtered_input - cmd_module = Config.CMD_DICT.get(plugin) - if not cmd_module: - await message.reply(text="Invalid cmd.") - return - module = str(cmd_module.func.__module__) - else: - file_name: str = os.path.splitext(message.replied.document.file_name)[0] - module = f"app.temp.{file_name}" - await message.replied.download("app/temp/") - reply: Message = await message.reply("Loading....") - reload = sys.modules.pop(module, None) - status: str = "Reloaded" if reload else "Loaded" - try: - importlib.import_module(module) - except Exception: - await reply.edit(str(traceback.format_exc())) - return - await reply.edit(f"{status} {module}") - - -if Config.DEV_MODE: - Config.CMD_DICT["load"] = Config.CMD( - cmd="load", - func=loader, - cmd_path=inspect.stack()[0][1], - sudo=False, - ) diff --git a/app/plugins/dev/shell.py b/app/plugins/dev/shell.py deleted file mode 100644 index a31b2ff..0000000 --- a/app/plugins/dev/shell.py +++ /dev/null @@ -1,65 +0,0 @@ -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.create_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): - 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: - await reply.edit( - text=f"```shell\n{stdout}```", - disable_web_page_preview=True, - parse_mode=ParseMode.MARKDOWN, - ) - output = stdout - if sleep_for >= 6: - sleep_for = 2 - await asyncio.create_task(asyncio.sleep(sleep_for), name=reply.task_id) - sleep_for += 2 - await reply.edit( - text=f"
~${cmd}\n\n{sub_process.full_std}
", - name="shell.txt", - disable_web_page_preview=True, - ) - except asyncio.exceptions.CancelledError: - sub_process.cancel() - await reply.edit(f"`Cancelled....`") - - -if Config.DEV_MODE: - Config.CMD_DICT["shell"] = Config.CMD( - cmd="shell", - func=live_shell, - cmd_path=inspect.stack()[0][1], - sudo=False, - ) - Config.CMD_DICT["sh"] = Config.CMD( - cmd="sh", - func=run_cmd, - cmd_path=inspect.stack()[0][1], - sudo=False, - ) diff --git a/app/plugins/files/download.py b/app/plugins/files/download.py index 7ada02f..4ddf672 100644 --- a/app/plugins/files/download.py +++ b/app/plugins/files/download.py @@ -2,11 +2,11 @@ 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 +from ub_core.utils.downloader import Download, DownloadedFile +from ub_core.utils.helpers import progress +from ub_core.utils.media_helper import get_tg_media_details + +from app import BOT, Message, bot @bot.add_cmd(cmd="download") diff --git a/app/plugins/files/rename.py b/app/plugins/files/rename.py index 37061b3..fc69f7f 100644 --- a/app/plugins/files/rename.py +++ b/app/plugins/files/rename.py @@ -3,12 +3,12 @@ import os import shutil import time -from app import BOT, bot -from app.core import Message +from ub_core.utils.downloader import Download, DownloadedFile +from ub_core.utils.helpers import progress + +from app import BOT, Message, bot 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") diff --git a/app/plugins/files/upload.py b/app/plugins/files/upload.py index aa01dd1..ce14f11 100644 --- a/app/plugins/files/upload.py +++ b/app/plugins/files/upload.py @@ -2,12 +2,12 @@ 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 +from ub_core.utils.downloader import Download, DownloadedFile +from ub_core.utils.helpers import progress +from ub_core.utils.media_helper import MediaType, bytes_to_mb +from ub_core.utils.shell import check_audio, get_duration, take_ss + +from app import BOT, Config, Message, bot async def video_upload( diff --git a/app/plugins/misc/gemini.py b/app/plugins/misc/gemini.py index 282775f..21f80ef 100644 --- a/app/plugins/misc/gemini.py +++ b/app/plugins/misc/gemini.py @@ -6,18 +6,18 @@ from pyrogram import filters from pyrogram.enums import ParseMode from pyrogram.types import Message as Msg -from app import BOT, Config, Convo, Message, bot +from app import BOT, Convo, Message, bot, extra_config MODEL = genai.GenerativeModel("gemini-pro") async def init_task(): - if Config.GEMINI_API_KEY: - genai.configure(api_key=Config.GEMINI_API_KEY) + if extra_config.GEMINI_API_KEY: + genai.configure(api_key=extra_config.GEMINI_API_KEY) async def basic_check(message: Message): - if not Config.GEMINI_API_KEY: + if not extra_config.GEMINI_API_KEY: await message.reply( "Gemini API KEY not found." "\nGet it HERE " @@ -88,7 +88,7 @@ async def ai_chat(bot: BOT, message: Message): ) return resp = await message.reply("Loading History...") - doc: BytesIO = (await reply.download(in_memory=True)).getbuffer() + doc: BytesIO = (await reply.download(in_memory=True)).getbuffer() # NOQA history = pickle.loads(doc) await resp.edit("History Loaded... Resuming chat") chat = MODEL.start_chat(history=history) diff --git a/app/plugins/misc/song.py b/app/plugins/misc/song.py index 7f430f4..ebf3407 100644 --- a/app/plugins/misc/song.py +++ b/app/plugins/misc/song.py @@ -6,9 +6,9 @@ from time import time from urllib.parse import urlparse import yt_dlp +from ub_core.utils.aiohttp_tools import aio from app import Message, bot -from app.utils.aiohttp_tools import aio domains = [ "www.youtube.com", diff --git a/app/plugins/sudo/users.py b/app/plugins/sudo/users.py index 7684095..1dbc937 100644 --- a/app/plugins/sudo/users.py +++ b/app/plugins/sudo/users.py @@ -1,9 +1,9 @@ import asyncio from pyrogram.types import User +from ub_core.utils.helpers import extract_user_data, get_name from app import BOT, Config, CustomDB, Message, bot -from app.utils.helpers import extract_user_data, get_name SUDO = CustomDB("COMMON_SETTINGS") SUDO_USERS = CustomDB("SUDO_USERS") diff --git a/app/plugins/sys_utils/logs.py b/app/plugins/sys_utils/logs.py deleted file mode 100644 index cadf7c2..0000000 --- a/app/plugins/sys_utils/logs.py +++ /dev/null @@ -1,14 +0,0 @@ -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"
{text}
") - else: - await message.reply_document(document="logs/app_logs.txt") diff --git a/app/plugins/sys_utils/restart.py b/app/plugins/sys_utils/restart.py deleted file mode 100644 index c52a69e..0000000 --- a/app/plugins/sys_utils/restart.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -import shutil - -from pyrogram.enums import ChatType - -from app import BOT, Message, bot - - -async def init_task() -> 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 bot.get_chat(restart_chat) - await bot.edit_message_text( - chat_id=restart_chat, message_id=restart_msg, text="__Started__" - ) - os.environ.pop("RESTART_MSG") - os.environ.pop("RESTART_CHAT") - - -@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. - -cl: for clearing logs. - -cp: for clearing temp plugins. - 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) - if "-cl" in message.flags: - os.remove("logs/app_logs.txt") - if "-cp" in message.flags: - shutil.rmtree("app/temp") - await bot.restart(hard="-h" in message.flags) diff --git a/app/plugins/sys_utils/update.py b/app/plugins/sys_utils/update.py deleted file mode 100644 index cba0d7f..0000000 --- a/app/plugins/sys_utils/update.py +++ /dev/null @@ -1,73 +0,0 @@ -import asyncio - -from git import Repo - -from app import BOT, Config, Message, bot -from app.plugins.sys_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"#{commit.count()} " - f"{commit.message} " - f"By {commit.author}" - ) - limit += 1 - if limit >= 15: - 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(text="Already Up To Date.", del_in=5) - return - if "-pull" not in message.flags: - await reply.edit( - text=f"Update Available:\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( - text=f"#Updater\nPulled:\n{commits}", disable_web_page_preview=True - ), - reply.edit("Update Found\nPulling...."), - ) - await restart(bot, message, reply) diff --git a/app/plugins/tg_tools/delete.py b/app/plugins/tg_tools/delete.py index 9856c23..8eb4c9b 100644 --- a/app/plugins/tg_tools/delete.py +++ b/app/plugins/tg_tools/delete.py @@ -1,5 +1,4 @@ -from app import BOT, bot -from app.core import Message +from app import BOT, Message, bot from app.plugins.tg_tools.get_message import parse_link diff --git a/app/plugins/tg_tools/kang.py b/app/plugins/tg_tools/kang.py index a17107d..13f6774 100644 --- a/app/plugins/tg_tools/kang.py +++ b/app/plugins/tg_tools/kang.py @@ -10,11 +10,11 @@ from pyrogram.enums import MessageMediaType from pyrogram.errors import StickersetInvalid from pyrogram.raw.functions.messages import GetStickerSet from pyrogram.raw.types import InputStickerSetShortName +from ub_core.utils.helpers import get_name +from ub_core.utils.media_helper import MediaExts +from ub_core.utils.shell import get_duration, run_shell_cmd from app import BOT, Message, bot -from app.utils.helpers import get_name -from app.utils.media_helper import MediaExts -from app.utils.shell import get_duration, run_shell_cmd EMOJIS = ("☕", "🤡", "🙂", "🤔", "🔪", "😂", "💀") diff --git a/app/plugins/tg_tools/pm_n_tag_logger.py b/app/plugins/tg_tools/pm_n_tag_logger.py index 3026373..2f93176 100644 --- a/app/plugins/tg_tools/pm_n_tag_logger.py +++ b/app/plugins/tg_tools/pm_n_tag_logger.py @@ -4,9 +4,9 @@ from collections import defaultdict from pyrogram import filters from pyrogram.enums import ChatType, MessageEntityType from pyrogram.errors import MessageIdInvalid +from ub_core.utils.helpers import get_name -from app import BOT, Config, CustomDB, Message, bot -from app.utils.helpers import get_name +from app import BOT, Config, CustomDB, Message, bot, extra_config LOGGER = CustomDB("COMMON_SETTINGS") @@ -19,10 +19,10 @@ async def init_task(): tag_check = await LOGGER.find_one({"_id": "tag_logger_switch"}) pm_check = await LOGGER.find_one({"_id": "pm_logger_switch"}) if tag_check: - Config.TAG_LOGGER = tag_check["value"] + extra_config.TAG_LOGGER = tag_check["value"] if pm_check: - Config.PM_LOGGER = pm_check["value"] - Config.MESSAGE_LOGGER_TASK = asyncio.create_task(runner()) + extra_config.PM_LOGGER = pm_check["value"] + Config.BACKGROUND_TASKS.append(asyncio.create_task(runner(), name="pm_tag_logger")) @bot.add_cmd(cmd=["taglogger", "pmlogger"]) @@ -36,12 +36,12 @@ async def logger_switch(bot: BOT, message: Message): conf_str = f"{text.upper()}_LOGGER" if "-c" in message.flags: await message.reply( - text=f"{text.capitalize()} Logger is enabled: {getattr(Config, conf_str)}!", + text=f"{text.capitalize()} Logger is enabled: {getattr(extra_config, conf_str)}!", del_in=8, ) return - value: bool = not getattr(Config, conf_str) - setattr(Config, conf_str, value) + value: bool = not getattr(extra_config, conf_str) + setattr(extra_config, conf_str, value) await asyncio.gather( LOGGER.add_data({"_id": f"{text}_logger_switch", "value": value}), message.reply( @@ -51,8 +51,11 @@ async def logger_switch(bot: BOT, message: Message): text=f"#{text.capitalize()}Logger is enabled: {value}!", type="info" ), ) - if not Config.MESSAGE_LOGGER_TASK or Config.MESSAGE_LOGGER_TASK.done(): - Config.MESSAGE_LOGGER_TASK = asyncio.create_task(runner()) + for task in Config.BACKGROUND_TASKS: + if task.get_name() == "pm_tag_logger" and not task.done(): + Config.BACKGROUND_TASKS.append( + asyncio.create_task(runner(), name="pm_tag_logger") + ) basic_filters = ( @@ -67,14 +70,14 @@ basic_filters = ( @bot.on_message( filters=basic_filters & filters.private - & filters.create(lambda _, __, ___: Config.PM_LOGGER), + & filters.create(lambda _, __, ___: extra_config.PM_LOGGER), group=2, ) async def pm_logger(bot: BOT, message: Message): cache_message(message) -tag_filter = filters.create(lambda _, __, ___: Config.TAG_LOGGER) +tag_filter = filters.create(lambda _, __, ___: extra_config.TAG_LOGGER) @bot.on_message( @@ -128,7 +131,7 @@ def cache_message(message: Message): async def runner(): - if not (Config.TAG_LOGGER or Config.PM_LOGGER): + if not (extra_config.TAG_LOGGER or extra_config.PM_LOGGER): return last_pm_logged_id = 0 while True: @@ -158,11 +161,11 @@ async def runner(): async def log_pm(message: Message, log_info: bool): if log_info: await bot.send_message( - chat_id=Config.MESSAGE_LOGGER_CHAT, + chat_id=extra_config.MESSAGE_LOGGER_CHAT, text=f"#PM\n{message.from_user.mention} [{message.from_user.id}]", ) try: - await message.forward(Config.MESSAGE_LOGGER_CHAT) + await message.forward(extra_config.MESSAGE_LOGGER_CHAT) except MessageIdInvalid: notice = ( f"{message.from_user.mention} [{message.from_user.id}] deleted this message." @@ -172,7 +175,7 @@ async def log_pm(message: Message, log_info: bool): f"Caption:\n{message.caption or 'No Caption in media.'}" ) - await message.copy(Config.MESSAGE_LOGGER_CHAT, caption=notice) + await message.copy(extra_config.MESSAGE_LOGGER_CHAT, caption=notice) async def log_chat(message: Message): @@ -190,15 +193,15 @@ async def log_chat(message: Message): if message.reply_to_message: try: - await message.reply_to_message.forward(Config.MESSAGE_LOGGER_CHAT) + await message.reply_to_message.forward(extra_config.MESSAGE_LOGGER_CHAT) except MessageIdInvalid: await message.reply_to_message.copy( - Config.MESSAGE_LOGGER_CHAT, caption=notice + extra_config.MESSAGE_LOGGER_CHAT, caption=notice ) try: - logged = await message.forward(Config.MESSAGE_LOGGER_CHAT) + logged = await message.forward(extra_config.MESSAGE_LOGGER_CHAT) await logged.reply( text=f"#TAG\n{mention} [{u_id}]\nMessage: \n{message.chat.title} ({message.chat.id})", ) except MessageIdInvalid: - await message.copy(Config.MESSAGE_LOGGER_CHAT, caption=notice) + await message.copy(extra_config.MESSAGE_LOGGER_CHAT, caption=notice) diff --git a/app/plugins/tg_tools/pm_permit.py b/app/plugins/tg_tools/pm_permit.py index 13f82ea..69d7cde 100644 --- a/app/plugins/tg_tools/pm_permit.py +++ b/app/plugins/tg_tools/pm_permit.py @@ -3,9 +3,9 @@ from collections import defaultdict from pyrogram import filters from pyrogram.enums import ChatType +from ub_core.utils.helpers import get_name -from app import BOT, Config, CustomDB, Message, bot -from app.utils.helpers import get_name +from app import BOT, CustomDB, Message, bot, extra_config PM_USERS = CustomDB("PM_USERS") @@ -15,15 +15,15 @@ ALLOWED_USERS: list[int] = [] allowed_filter = filters.create(lambda _, __, m: m.chat.id in ALLOWED_USERS) -guard_check = filters.create(lambda _, __, ___: Config.PM_GUARD) +guard_check = filters.create(lambda _, __, ___: extra_config.PM_GUARD) RECENT_USERS: dict = defaultdict(int) async def init_task(): guard = (await PM_GUARD.find_one({"_id": "guard_switch"})) or {} + extra_config.PM_GUARD = guard.get("value", False) [ALLOWED_USERS.append(user_id["_id"]) async for user_id in PM_USERS.find()] - Config.PM_GUARD = guard.get("value", False) @bot.on_message( @@ -82,11 +82,11 @@ async def pmguard(bot: BOT, message: Message): """ if "-c" in message.flags: await message.reply( - text=f"PM Guard is enabled: {Config.PM_GUARD}", del_in=8 + text=f"PM Guard is enabled: {extra_config.PM_GUARD}", del_in=8 ) return - value = not Config.PM_GUARD - Config.PM_GUARD = value + value = not extra_config.PM_GUARD + extra_config.PM_GUARD = value await asyncio.gather( PM_GUARD.add_data({"_id": "guard_switch", "value": value}), message.reply(text=f"PM Guard is enabled: {value}!", del_in=8), diff --git a/app/utils/__init__.py b/app/utils/__init__.py deleted file mode 100644 index 13e1996..0000000 --- a/app/utils/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -import json - - -class Str: - def __str__(self): - return json.dumps(self.__dict__, indent=4, ensure_ascii=False, default=str) diff --git a/app/utils/aiohttp_tools.py b/app/utils/aiohttp_tools.py deleted file mode 100644 index bd7d826..0000000 --- a/app/utils/aiohttp_tools.py +++ /dev/null @@ -1,82 +0,0 @@ -import json -import logging -import os -from io import BytesIO - -from aiohttp import ClientSession, ContentTypeError, web - -from app import Config -from app.utils.media_helper import get_filename_from_url - -LOGGER = logging.getLogger(Config.BOT_NAME) - - -class Aio: - def __init__(self): - self.session: ClientSession | None = None - self.app = None - self.port = os.environ.get("API_PORT", 0) - self.runner = None - if self.port: - Config.INIT_TASKS.append(self.set_site()) - Config.INIT_TASKS.append(self.set_session()) - - async def close(self): - if not self.session.closed: - await self.session.close() - if self.runner: - await self.runner.cleanup() - - async def set_session(self): - self.session = ClientSession() - - async def set_site(self): - LOGGER.info("Starting Static WebSite.") - self.app = web.Application() - self.app.router.add_get(path="/", handler=self.handle_request) - self.runner = web.AppRunner(self.app) - await self.runner.setup() - site = web.TCPSite( - self.runner, "0.0.0.0", self.port, reuse_address=True, reuse_port=True - ) - await site.start() - - @staticmethod - async def handle_request(_): - return web.Response(text="Web Server Running...") - - async def get_json( - self, - url: str, - headers: dict = None, - params: dict | str = None, - json_: bool = False, - timeout: int = 10, - ) -> dict | None: - try: - async with self.session.get( - url=url, headers=headers, params=params, timeout=timeout - ) as ses: - if json_: - return await ses.json() - else: - return (json.loads(await ses.text())) # fmt:skip - except (json.JSONDecodeError, ContentTypeError): - LOGGER.debug(await ses.text()) - except TimeoutError: - LOGGER.debug("Timeout") - - async def in_memory_dl(self, url: str) -> BytesIO: - async with self.session.get(url) as remote_file: - bytes_data = await remote_file.read() - file = BytesIO(bytes_data) - file.name = get_filename_from_url(url, tg_safe=True) - return file - - async def thumb_dl(self, thumb) -> BytesIO | str | None: - if not thumb or not thumb.startswith("http"): - return thumb - return (await self.in_memory_dl(thumb)) # fmt:skip - - -aio = Aio() diff --git a/app/utils/downloader.py b/app/utils/downloader.py deleted file mode 100644 index 0f42f7d..0000000 --- a/app/utils/downloader.py +++ /dev/null @@ -1,179 +0,0 @@ -import asyncio -import os -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 import Str -from app.utils.helpers import progress -from app.utils.media_helper import (bytes_to_mb, get_filename_from_headers, - get_filename_from_url, get_type) - - -class DownloadedFile(Str): - 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) - - -class Download(Str): - """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) - - @classmethod - async def setup( - cls, - url: str, - path: str = "downloads", - message_to_edit: Message | None = None, - custom_file_name: str | None = None, - ) -> "Download": - session = aiohttp.ClientSession() - file_session = await session.get(url=url) - headers = file_session.headers - cls_object = 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 asyncio.gather( - cls_object.check_disk_space(), cls_object.check_duplicates() - ) - return cls_object - - 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 - return get_filename_from_headers( - self.headers) or get_filename_from_url( - 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, - ) diff --git a/app/utils/helpers.py b/app/utils/helpers.py deleted file mode 100644 index ba266dd..0000000 --- a/app/utils/helpers.py +++ /dev/null @@ -1,76 +0,0 @@ -import time - -from pyrogram.types import Chat, 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=Config.BOT_NAME, - author_name=Config.BOT_NAME, - author_url=Config.UPSTREAM_REPO, - ) - 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"

{text}

", - author_name=Config.BOT_NAME, - author_url=Config.UPSTREAM_REPO, - ) - return telegraph["url"] - - -def get_name(user_or_chat: User | Chat) -> str: - first = user_or_chat.first_name or "" - last = user_or_chat.last_name or "" - name = f"{first} {last}".strip() - if not name: - name = user_or_chat.title - return name - -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"{action}" - f"\n
"
-            f"\nfile={file_name}"
-            f"\npath={file_path}"
-            f"\nsize={bytes_to_mb(total)}mb"
-            f"\ncompleted={bytes_to_mb(current)}mb | {percentage}%
" - ) diff --git a/app/utils/media_helper.py b/app/utils/media_helper.py deleted file mode 100644 index 7b0ef3c..0000000 --- a/app/utils/media_helper.py +++ /dev/null @@ -1,94 +0,0 @@ -import re -from enum import Enum, auto -from os.path import basename, splitext -from urllib.parse import unquote_plus, 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_from_url(url: str, tg_safe: bool = False) -> str: - parsed_url = urlparse(unquote_plus(url)) - name = basename(parsed_url.path.rstrip("/")) - if tg_safe: - return make_file_name_tg_safe(file_name=name) - return name - - -def get_filename_from_headers(headers: dict, tg_safe: bool = False) -> str | None: - content_disposition = headers.get("Content-Disposition", "") - match = re.search(r"filename=(.+)", content_disposition) - if not match: - return - if tg_safe: - return make_file_name_tg_safe(file_name=match.group(1)) - return match.group(1) - - -def make_file_name_tg_safe(file_name: str) -> str: - if file_name.lower().endswith((".webp", ".heic")): - file_name = file_name + ".jpg" - elif file_name.lower().endswith(".webm"): - file_name = file_name + ".mp4" - return file_name - - -def get_type(url: str | None = "", path: str | None = "") -> MediaType | None: - if url: - media = get_filename_from_url(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 diff --git a/app/utils/shell.py b/app/utils/shell.py deleted file mode 100644 index 8f3ee42..0000000 --- a/app/utils/shell.py +++ /dev/null @@ -1,74 +0,0 @@ -import asyncio -import os - - -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: - thumb = f"{path}/i.png" - await run_shell_cmd( - f'''ffmpeg -hide_banner -loglevel error -ss 0.1 -i "{video}" -vframes 1 "{thumb}"''' - ) - if os.path.isfile(thumb): - return thumb - - -async def check_audio(file) -> int: - result = await run_shell_cmd( - f'''ffprobe -v error -show_entries format=nb_streams -of default=noprint_wrappers=1:nokey=1 "{file}"''' - ) - return int(result or 0) - 1 - - -async def get_duration(file) -> int: - duration = await run_shell_cmd( - f'''ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "{file}"''' - ) - return round(float(duration.strip() or 0)) - - -class AsyncShell: - def __init__( - self, - process: asyncio.create_subprocess_shell, - ): - self.process: asyncio.create_subprocess_shell = process - self.full_std: str = "" - self.last_line: str = "" - self.is_done: bool = False - self._task: asyncio.Task | None = None - - async def read_output(self) -> None: - async for line in self.process.stdout: - decoded_line = line.decode("utf-8") - self.full_std += decoded_line - self.last_line = decoded_line - self.is_done = True - await self.process.wait() - - async def get_output(self): - while not self.is_done: - yield self.full_std if len(self.full_std) < 4000 else self.last_line - await asyncio.sleep(0) - - def cancel(self) -> None: - if not self.is_done: - self.process.kill() - self._task.cancel() - - @classmethod - async def run_cmd(cls, cmd: str, name: str = "AsyncShell") -> "AsyncShell": - sub_process: AsyncShell = cls( - process=await asyncio.create_subprocess_shell( - cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT - ) - ) - sub_process._task = asyncio.create_task(sub_process.read_output(), name=name) - await asyncio.sleep(0.5) - return sub_process diff --git a/req.txt b/req.txt index cc063a0..c895672 100644 --- a/req.txt +++ b/req.txt @@ -1,16 +1,7 @@ -aiohttp==3.8.5 -aiofiles==23.2.1 -async-lru==2.0.4 -httpx==0.25.0 -dnspython==2.3.0 -motor==3.2.0 -pymongo==4.4.0 -gitpython>=3.1.32 -pyrogram==2.0.106 -python-dotenv==0.21.0 -tgcrypto -telegraph==2.2.0 -uvloop==0.17.0 +# BoilerPlate Code for UB +git+https://github.com/thedragonsinn/ub-core.git + yt-dlp pillow google-generativeai +