commit 32db859d7e67be460862037ddd0ff5d91514f002
Author: thedragonsinn <98635854+thedragonsinn@users.noreply.github.com>
Date: Mon Sep 25 18:15:38 2023 +0530
Initial Commit.
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dff707c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+config*.env
+*session*
+venv/
+__pycache__
+.idea/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..eb21955
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,22 @@
+FROM python:3.11.2
+
+WORKDIR /app/
+
+COPY req.txt .
+
+RUN sed -i.bak 's/us-west-2\.ec2\.//' /etc/apt/sources.list && \
+ apt -qq update && apt -qq upgrade -y && \
+ apt -qq install -y --no-install-recommends \
+ apt-utils \
+ curl \
+ git \
+ wget && \
+ pip install -U pip setuptools wheel && \
+ pip install -r req.txt && \
+ rm req.txt && \
+ git config --global user.email "98635854+thedragonsinn@users.noreply.github.com" && \
+ git config --global user.name "thedragonsinn"
+
+EXPOSE 8080
+
+CMD bash -c "$(curl -fsSL https://raw.githubusercontent.com/thedragonsinn/plain-ub/main/docker_start_cmd)"
\ No newline at end of file
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..99dcd42
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,15 @@
+import os
+
+from dotenv import load_dotenv
+load_dotenv("config.env")
+
+from app.config import Config
+from app.core.db import DB
+from app.core.client.client import BOT
+
+if "com.termux" not in os.environ.get("PATH", ""):
+ import uvloop
+
+ uvloop.install()
+
+bot = BOT()
diff --git a/app/__main__.py b/app/__main__.py
new file mode 100644
index 0000000..97df1fd
--- /dev/null
+++ b/app/__main__.py
@@ -0,0 +1,8 @@
+if __name__ == "__main__":
+ import tracemalloc
+
+ tracemalloc.start()
+
+ from app import bot
+
+ bot.run(bot.boot())
diff --git a/app/config.py b/app/config.py
new file mode 100644
index 0000000..feba87e
--- /dev/null
+++ b/app/config.py
@@ -0,0 +1,23 @@
+import json
+import os
+from typing import Coroutine
+
+
+class Config:
+ CMD_DICT: dict["str", Coroutine] = {}
+
+ CALLBACK_DICT: dict["str", Coroutine] = {}
+
+ DEV_MODE: int = int(os.environ.get("DEV_MODE", 0))
+
+ DB_URL: str = os.environ.get("DB_URL")
+
+ LOG_CHAT: int = int(os.environ.get("LOG_CHAT"))
+
+ TRIGGER: str = os.environ.get("TRIGGER", ".")
+
+ USERS: list[int] = json.loads(os.environ.get("USERS", "[]"))
+
+ UPSTREAM_REPO: str = os.environ.get(
+ "UPSTREAM_REPO", "https://github.com/thedragonsinn/plain-ub"
+ )
diff --git a/app/core/__init__.py b/app/core/__init__.py
new file mode 100644
index 0000000..6ef3717
--- /dev/null
+++ b/app/core/__init__.py
@@ -0,0 +1,3 @@
+from app.core.client import filters
+from app.core.types.callback_query import CallbackQuery
+from app.core.types.message import Message
diff --git a/app/core/client/client.py b/app/core/client/client.py
new file mode 100644
index 0000000..e0e0d49
--- /dev/null
+++ b/app/core/client/client.py
@@ -0,0 +1,125 @@
+import glob
+import importlib
+import os
+import sys
+from functools import wraps
+from io import BytesIO
+
+from pyrogram import Client, idle
+from pyrogram.enums import ParseMode
+from pyrogram.types import Message as Msg
+
+from app import DB, Config
+from app.core import Message
+from app.utils import aiohttp_tools
+
+
+async 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("/", ".")
+ importlib.import_module(py_name)
+
+
+class BOT(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"),
+ in_memory=True,
+ parse_mode=ParseMode.DEFAULT,
+ sleep_threshold=30,
+ max_concurrent_transmissions=2,
+ )
+
+ @staticmethod
+ def add_cmd(cmd: str, cb: bool = False):
+ def the_decorator(func):
+ @wraps(func)
+ def wrapper():
+ config_dict = Config.CMD_DICT
+ if cb:
+ config_dict = Config.CALLBACK_DICT
+ if isinstance(cmd, list):
+ for _cmd in cmd:
+ config_dict[_cmd] = func
+ else:
+ config_dict[cmd] = func
+
+ wrapper()
+ return func
+
+ return the_decorator
+
+ async def boot(self) -> None:
+ await super().start()
+ await import_modules()
+ await aiohttp_tools.session_switch()
+ await self.edit_restart_msg()
+ print("started")
+ await self.log(text="Started")
+ await idle()
+ await aiohttp_tools.session_switch()
+ DB._client.close()
+
+ 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="__Started__"
+ )
+ os.environ.pop("RESTART_MSG", "")
+ os.environ.pop("RESTART_CHAT", "")
+
+ async def log(
+ self,
+ text="",
+ traceback="",
+ chat=None,
+ func=None,
+ message: Message | Msg | None = None,
+ name="log.txt",
+ disable_web_page_preview=True,
+ parse_mode=ParseMode.HTML,
+ ) -> Message | Msg:
+ if message:
+ return await message.copy(chat_id=Config.LOG_CHAT)
+ if traceback:
+ text = f"""
+#Traceback
+Function: {func}
+Chat: {chat}
+Traceback:
+{traceback}"""
+ return await self.send_message(
+ chat_id=Config.LOG_CHAT,
+ text=text,
+ name=name,
+ disable_web_page_preview=disable_web_page_preview,
+ parse_mode=parse_mode,
+ )
+
+ async def restart(self, hard=False) -> None:
+ await aiohttp_tools.session_switch()
+ await super().stop(block=False)
+ DB._client.close()
+ if hard:
+ os.execl("/bin/bash", "/bin/bash", "run")
+ os.execl(sys.executable, sys.executable, "-m", "app")
+
+ async def send_message(
+ self, chat_id: int | str, text, name: str = "output.txt", **kwargs
+ ) -> Message | Msg:
+ text = str(text)
+ if len(text) < 4096:
+ return Message.parse_message(
+ (await super().send_message(chat_id=chat_id, text=text, **kwargs))
+ )
+ doc = BytesIO(bytes(text, encoding="utf-8"))
+ doc.name = name
+ kwargs.pop("disable_web_page_preview", "")
+ return await super().send_document(chat_id=chat_id, document=doc, **kwargs)
diff --git a/app/core/client/filters.py b/app/core/client/filters.py
new file mode 100644
index 0000000..e05e1e5
--- /dev/null
+++ b/app/core/client/filters.py
@@ -0,0 +1,22 @@
+from pyrogram import filters as _filters
+
+from app import Config
+
+
+def dynamic_cmd_filter(_, __, message) -> bool:
+ if (
+ not message.text
+ or not message.text.startswith(Config.TRIGGER)
+ or not message.from_user
+ or message.from_user.id not in Config.USERS
+ ):
+ return False
+
+ start_str = message.text.split(maxsplit=1)[0]
+ cmd = start_str.replace(Config.TRIGGER, "", 1)
+ cmd_check = cmd in Config.CMD_DICT
+ reaction_check = not message.reactions
+ return bool(cmd_check and reaction_check)
+
+
+cmd_filter = _filters.create(dynamic_cmd_filter)
diff --git a/app/core/client/handler.py b/app/core/client/handler.py
new file mode 100644
index 0000000..6eeab4e
--- /dev/null
+++ b/app/core/client/handler.py
@@ -0,0 +1,50 @@
+import asyncio
+import traceback
+from datetime import datetime
+
+from pyrogram.enums import ChatType
+
+from app import DB, Config, bot
+from app.core import CallbackQuery, Message, filters
+
+
+@bot.on_message(filters.cmd_filter)
+@bot.on_edited_message(filters.cmd_filter)
+async def cmd_dispatcher(bot, message) -> None:
+ message = Message.parse_message(message)
+ func = Config.CMD_DICT[message.cmd]
+ coro = func(bot, message)
+ await run_coro(coro, message)
+
+
+@bot.on_callback_query()
+async def callback_handler(bot: bot, cb):
+ if (
+ cb.message.chat.type == ChatType.PRIVATE
+ and (datetime.now() - cb.message.date).total_seconds() > 30
+ ):
+ return await cb.edit_message_text(f"Query Expired. Try again.")
+ banned = await DB.BANNED.find_one({"_id": cb.from_user.id})
+ if banned:
+ return
+ cb = CallbackQuery.parse_cb(cb)
+ func = Config.CALLBACK_DICT.get(cb.cmd)
+ if not func:
+ return
+ coro = func(bot, cb)
+ await run_coro(coro, Message.parse_message(cb.message))
+
+
+async def run_coro(coro, message) -> None:
+ try:
+ task = asyncio.Task(coro, name=message.task_id)
+ await task
+ except asyncio.exceptions.CancelledError:
+ await bot.log(text=f"#Cancelled:\n{message.text}")
+ except BaseException:
+ await bot.log(
+ traceback=str(traceback.format_exc()),
+ chat=message.chat.title or message.chat.first_name,
+ func=coro.__name__,
+ name="traceback.txt",
+ )
diff --git a/app/core/db.py b/app/core/db.py
new file mode 100644
index 0000000..5b1d89a
--- /dev/null
+++ b/app/core/db.py
@@ -0,0 +1,25 @@
+import dns.resolver
+from motor.core import AgnosticClient, AgnosticCollection, AgnosticDatabase
+from motor.motor_asyncio import AsyncIOMotorClient
+
+from app import Config
+
+dns.resolver.default_resolver = dns.resolver.Resolver(configure=False)
+dns.resolver.default_resolver.nameservers = ["8.8.8.8"]
+
+
+class DataBase:
+ def __init__(self):
+ self._client: AgnosticClient = AsyncIOMotorClient(Config.DB_URL)
+ self.db: AgnosticDatabase = self._client["plain_ub"]
+ self.SUDO_USERS: AgnosticCollection = self.db.USERS
+
+ def __getattr__(self, attr) -> AgnosticCollection:
+ try:
+ return self.__dict__[attr]
+ except KeyError:
+ self.__dict__[attr] = self.db[attr]
+ return self.__dict__[attr]
+
+
+DB = DataBase()
diff --git a/app/core/types/callback_query.py b/app/core/types/callback_query.py
new file mode 100644
index 0000000..ddbebff
--- /dev/null
+++ b/app/core/types/callback_query.py
@@ -0,0 +1,26 @@
+import json
+from functools import cached_property
+
+from pyrogram.types import CallbackQuery as Callback_Query
+
+
+class CallbackQuery(Callback_Query):
+ def __init__(self, query: Callback_Query):
+ super().__dict__.update(query.__dict__)
+
+ @cached_property
+ def cmd(self) -> str | None:
+ return self.cb_data.get("cmd")
+
+ @cached_property
+ def cb_data(self) -> dict:
+ if not self.data:
+ return {}
+ try:
+ return json.loads(self.data.replace("'", '"'))
+ except BaseException:
+ return {}
+
+ @classmethod
+ def parse_cb(cls, cb) -> "CallbackQuery":
+ return cls(cb)
diff --git a/app/core/types/message.py b/app/core/types/message.py
new file mode 100644
index 0000000..1956118
--- /dev/null
+++ b/app/core/types/message.py
@@ -0,0 +1,128 @@
+import asyncio
+from functools import cached_property
+
+from pyrogram.errors import MessageDeleteForbidden
+from pyrogram.types import Message as Msg
+from pyrogram.types import User
+
+from app import Config
+
+
+class Message(Msg):
+ def __init__(self, message: Msg) -> None:
+ super().__dict__.update(message.__dict__)
+
+ @cached_property
+ def cmd(self) -> str | None:
+ raw_cmd = self.text_list[0]
+ cmd = raw_cmd.lstrip(Config.TRIGGER)
+ return cmd if cmd in Config.CMD_DICT else None
+
+ @cached_property
+ def flags(self) -> list:
+ return [i for i in self.text_list if i.startswith("-")]
+
+ @cached_property
+ def flt_input(self) -> str:
+ split_lines = self.input.splitlines()
+ split_n_joined = [
+ " ".join([word for word in line.split(" ") if word not in self.flags])
+ for line in split_lines
+ ]
+ return "\n".join(split_n_joined)
+
+ @cached_property
+ def input(self) -> str:
+ if len(self.text_list) > 1:
+ return self.text.split(maxsplit=1)[-1]
+ return ""
+
+ @cached_property
+ def replied(self) -> "Message":
+ if self.reply_to_message:
+ return Message.parse_message(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 task_id(self) -> str:
+ return f"{self.chat.id}-{self.id}"
+
+ @cached_property
+ def text_list(self) -> list:
+ return self.text.split()
+
+ async def async_deleter(self, del_in, task, block) -> None:
+ if block:
+ x = await task
+ await asyncio.sleep(del_in)
+ await x.delete()
+ 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, **kwargs) -> "Message":
+ if len(str(text)) < 4096:
+ kwargs.pop("name", "")
+ task = self.edit_text(text, **kwargs)
+ if del_in:
+ reply = await self.async_deleter(task=task, del_in=del_in, block=block)
+ else:
+ reply = await task
+ else:
+ _, reply = await asyncio.gather(
+ super().delete(), self.reply(text, **kwargs)
+ )
+ return reply
+
+ async def extract_user_n_reason(self) -> list[User | str | Exception, str | None]:
+ if self.replied:
+ return [self.replied.from_user, self.flt_input]
+ inp_list = self.flt_input.split(maxsplit=1)
+ if not inp_list:
+ return [
+ "Unable to Extract User info.\nReply to a user or input @ | id.",
+ "",
+ ]
+ user = inp_list[0]
+ reason = None
+ if len(inp_list) >= 2:
+ reason = inp_list[1]
+ 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 as e:
+ return [e, reason]
+
+ 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 await task
+
+ @classmethod
+ def parse_message(cls, message: Msg) -> "Message":
+ return cls(message)
diff --git a/app/plugins/admin_tools.py b/app/plugins/admin_tools.py
new file mode 100644
index 0000000..0232039
--- /dev/null
+++ b/app/plugins/admin_tools.py
@@ -0,0 +1,112 @@
+from typing import Awaitable
+
+from pyrogram.types import ChatPermissions, ChatPrivileges, User
+
+from app import bot
+from app.core import Message
+
+
+def get_privileges(
+ anon: bool = False, full: bool = False, demote: bool = False
+) -> ChatPrivileges:
+ if demote:
+ return ChatPrivileges(
+ can_manage_chat=False,
+ 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:
+ user, title = await message.extract_user_n_reason()
+ if not isinstance(user, User):
+ await message.reply(user, del_in=10)
+ return
+ full: bool = "-f" in message.flags
+ anon: bool = "-anon" in message.flags
+ demote = message.cmd == "demote"
+ privileges: ChatPrivileges = get_privileges(full=full, anon=anon, demote=demote)
+ response = 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 not demote:
+ await bot.set_administrator_title(
+ chat_id=message.chat.id, user_id=user.id, title=title or "Admin"
+ )
+ if title:
+ response += f"\nTitle: {title}."
+ await message.reply(text=response)
+ except Exception as e:
+ await message.reply(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()}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
+ word = "Unmuted"
+ 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,
+ )
+ word = "Muted"
+ try:
+ await bot.restrict_chat_member(
+ chat_id=message.chat.id, user_id=user.id, permissions=perms
+ )
+ await message.reply(text=f"{word}: {user.mention}\nReason: {reason}.")
+ except Exception as e:
+ await message.reply(text=e, del_in=10)
diff --git a/app/plugins/dev_tools.py b/app/plugins/dev_tools.py
new file mode 100644
index 0000000..08c41bd
--- /dev/null
+++ b/app/plugins/dev_tools.py
@@ -0,0 +1,142 @@
+import asyncio
+import importlib
+import sys
+import traceback
+from io import StringIO
+
+from pyrogram.enums import ParseMode
+
+from app.core import Message
+
+from app import Config, bot, DB # isort:skip
+from app.utils 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 = "\n\n".join([output, str(func_out)]).strip()
+ if "-s" not in message.flags:
+ output = f"> `{code}`\n\n>> `{output}`"
+ else:
+ return await reply.delete()
+ 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
diff --git a/app/plugins/tg_utils.py b/app/plugins/tg_utils.py
new file mode 100644
index 0000000..68d44b0
--- /dev/null
+++ b/app/plugins/tg_utils.py
@@ -0,0 +1,88 @@
+import os
+
+from pyrogram.enums import ChatType
+from pyrogram.errors import BadRequest
+
+from app import Config, bot
+from app.core import Message
+
+# 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) -> None:
+ chat: str = message.input
+ try:
+ await bot.join_chat(chat)
+ except (KeyError, BadRequest):
+ try:
+ await bot.join_chat(os.path.basename(chat).strip())
+ await message.reply("Joined")
+ except Exception as e:
+ await message.reply(str(e))
+
+
+@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,
+ )
diff --git a/app/plugins/utils.py b/app/plugins/utils.py
new file mode 100644
index 0000000..4cf6e89
--- /dev/null
+++ b/app/plugins/utils.py
@@ -0,0 +1,64 @@
+import asyncio
+import os
+
+from git import Repo
+from pyrogram.enums import ChatType
+
+from app import Config, bot
+from app.core import Message
+
+
+@bot.add_cmd(cmd="help")
+async def cmd_list(bot: bot, message: Message) -> None:
+ commands: str = "\n".join(
+ [f"{Config.TRIGGER}{i}" for i in Config.CMD_DICT.keys()]
+ )
+ await message.reply(f"Available Commands:\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="repo")
+async def sauce(bot: bot, message: Message) -> None:
+ await bot.send_message(
+ chat_id=message.chat.id,
+ text=f"Plain-UB.",
+ 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"""
+#{commit.count()} {commit.summary} By {commit.author}
+"""
+ limit += 1
+ if limit > 50:
+ break
+ if not commits:
+ return await reply.edit("Already Up To Date.", del_in=5)
+ if "-pull" not in message.flags:
+ return await reply.edit(
+ f"Update Available:\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("Update Found\nPulling...."),
+ )
+ await restart(bot, message, reply)
diff --git a/app/utils/aiohttp_tools.py b/app/utils/aiohttp_tools.py
new file mode 100644
index 0000000..d33d90a
--- /dev/null
+++ b/app/utils/aiohttp_tools.py
@@ -0,0 +1,77 @@
+import json
+from enum import Enum, auto
+from io import BytesIO
+from os.path import basename, splitext
+from urllib.parse import urlparse
+
+import aiohttp
+
+SESSION: aiohttp.ClientSession | None = None
+
+
+class MediaType(Enum):
+ PHOTO = auto()
+ VIDEO = auto()
+ GROUP = auto()
+ GIF = auto()
+ MESSAGE = auto()
+
+
+async def session_switch() -> None:
+ if not SESSION:
+ globals().update({"SESSION": aiohttp.ClientSession()})
+ else:
+ await SESSION.close()
+
+
+async def get_json(
+ url: str,
+ headers: dict = None,
+ params: dict = None,
+ json_: bool = False,
+ timeout: int = 10,
+) -> dict | None:
+ try:
+ async with SESSION.get(
+ url=url, headers=headers, params=params, timeout=timeout
+ ) as ses:
+ if json_:
+ ret_json = await ses.json()
+ else:
+ ret_json = json.loads(await ses.text())
+ return ret_json
+ except BaseException:
+ return
+
+
+async def in_memory_dl(url: str) -> BytesIO:
+ async with SESSION.get(url) as remote_file:
+ bytes_data = await remote_file.read()
+ file = BytesIO(bytes_data)
+ file.name = get_filename(url)
+ 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:
+ if not thumb or not thumb.startswith("http"):
+ return thumb
+ return await in_memory_dl(thumb)
diff --git a/app/utils/db_utils.py b/app/utils/db_utils.py
new file mode 100644
index 0000000..58e6e18
--- /dev/null
+++ b/app/utils/db_utils.py
@@ -0,0 +1,21 @@
+def extract_user_data(user) -> dict:
+ return dict(
+ name=f"""{user.first_name or ""} {user.last_name or ""}""",
+ username=user.username,
+ mention=user.mention,
+ )
+
+
+async def add_data(collection, id: int | str, data: dict) -> None:
+ found = await collection.find_one({"_id": id})
+ if not found:
+ await collection.insert_one({"_id": id, **data})
+ else:
+ await collection.update_one({"_id": id}, {"$set": data})
+
+
+async def delete_data(collection, id: int | str) -> bool | None:
+ found = await collection.find_one({"_id": id})
+ if found:
+ await collection.delete_one({"_id": id})
+ return True
diff --git a/app/utils/helpers.py b/app/utils/helpers.py
new file mode 100644
index 0000000..66735b6
--- /dev/null
+++ b/app/utils/helpers.py
@@ -0,0 +1,7 @@
+from pyrogram.types import User
+
+
+def get_name(user: User) -> str:
+ first = user.first_name or ""
+ last = user.last_name or ""
+ return f"{first} {last}".strip()
diff --git a/app/utils/shell.py b/app/utils/shell.py
new file mode 100644
index 0000000..231b360
--- /dev/null
+++ b/app/utils/shell.py
@@ -0,0 +1,47 @@
+import asyncio
+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")
+
+
+class AsyncShell:
+ def __init__(self, process: asyncio.create_subprocess_shell):
+ self.process: asyncio.create_subprocess_shell = process
+ self.full_std: str = ""
+ self.is_done: bool = False
+ self._task: asyncio.Task | None = None
+
+ async def read_output(self) -> None:
+ while True:
+ line: str = (await self.process.stdout.readline()).decode("utf-8")
+ if not line:
+ break
+ self.full_std += line
+ self.is_done = True
+ await self.process.wait()
+
+ async def get_output(self) -> AsyncIterable:
+ while not self.is_done:
+ yield self.full_std
+
+ 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/docker_start_cmd b/docker_start_cmd
new file mode 100644
index 0000000..ad85bfd
--- /dev/null
+++ b/docker_start_cmd
@@ -0,0 +1,9 @@
+#!bin/sh
+
+echo "${GH_TOKEN}" > ~/.git-credentials
+git config --global credential.helper store
+
+git clone -q --depth=1 "${UPSTREAM_REPO:-"https://github.com/thedragonsinn/plain-ub"}" ubot
+cd ubot
+#pip -q install --no-cache-dir -r req*.txt
+bash run
diff --git a/req.txt b/req.txt
new file mode 100644
index 0000000..e3cd783
--- /dev/null
+++ b/req.txt
@@ -0,0 +1,10 @@
+aiohttp==3.8.5
+async-lru==2.0.4
+motor==3.3.0
+pymongo==4.5.0
+dnspython==2.4.2
+gitpython>=3.1.32
+pyrogram==2.0.106
+python-dotenv==0.21.0
+tgCrypto==1.2.3
+uvloop==0.17.0
diff --git a/run b/run
new file mode 100755
index 0000000..f2e9391
--- /dev/null
+++ b/run
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+if [ "$API_PORT" ] ; then
+py_code="
+from aiohttp import web
+app = web.Application()
+app.router.add_get('/', lambda _: web.Response(text='Web Server Running...'))
+web.run_app(app, host='0.0.0.0', port=$API_PORT, reuse_port=True, print=None)
+"
+ python3 -q -c "$py_code" & echo "Dummy Web Server Started..."
+
+fi
+
+if ! [ -d ".git" ] ; then
+ git init
+fi
+
+python3 -m app
\ No newline at end of file
diff --git a/sample-config.env b/sample-config.env
new file mode 100644
index 0000000..a346021
--- /dev/null
+++ b/sample-config.env
@@ -0,0 +1,19 @@
+API_ID=
+
+API_HASH=
+
+DEV_MODE=0
+# exec, sh, shell commands
+
+DB_URL=
+# Mongo DB cluster URL
+
+LOG_CHAT=
+# Bot logs chat
+
+SESSION_STRING=""
+
+USERS = [1223478]
+# Separate multiple values with ,
+
+UPSTREAM_REPO = "https://github.com/thedragonsinn/plain-ub"
\ No newline at end of file