Initial Commit.
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
config*.env
|
||||
*session*
|
||||
venv/
|
||||
__pycache__
|
||||
.idea/
|
||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@@ -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)"
|
||||
15
app/__init__.py
Normal file
15
app/__init__.py
Normal file
@@ -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()
|
||||
8
app/__main__.py
Normal file
8
app/__main__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
if __name__ == "__main__":
|
||||
import tracemalloc
|
||||
|
||||
tracemalloc.start()
|
||||
|
||||
from app import bot
|
||||
|
||||
bot.run(bot.boot())
|
||||
23
app/config.py
Normal file
23
app/config.py
Normal file
@@ -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"
|
||||
)
|
||||
3
app/core/__init__.py
Normal file
3
app/core/__init__.py
Normal file
@@ -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
|
||||
125
app/core/client/client.py
Normal file
125
app/core/client/client.py
Normal file
@@ -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="<i>Started</i>")
|
||||
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
|
||||
<b>Function:</b> {func}
|
||||
<b>Chat:</b> {chat}
|
||||
<b>Traceback:</b>
|
||||
<code>{traceback}</code>"""
|
||||
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)
|
||||
22
app/core/client/filters.py
Normal file
22
app/core/client/filters.py
Normal file
@@ -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)
|
||||
50
app/core/client/handler.py
Normal file
50
app/core/client/handler.py
Normal file
@@ -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"<b>#Cancelled</b>:\n<code>{message.text}</code>")
|
||||
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",
|
||||
)
|
||||
25
app/core/db.py
Normal file
25
app/core/db.py
Normal file
@@ -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()
|
||||
26
app/core/types/callback_query.py
Normal file
26
app/core/types/callback_query.py
Normal file
@@ -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)
|
||||
128
app/core/types/message.py
Normal file
128
app/core/types/message.py
Normal file
@@ -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)
|
||||
112
app/plugins/admin_tools.py
Normal file
112
app/plugins/admin_tools.py
Normal file
@@ -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)
|
||||
142
app/plugins/dev_tools.py
Normal file
142
app/plugins/dev_tools.py
Normal file
@@ -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
|
||||
88
app/plugins/tg_utils.py
Normal file
88
app/plugins/tg_utils.py
Normal file
@@ -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,
|
||||
)
|
||||
64
app/plugins/utils.py
Normal file
64
app/plugins/utils.py
Normal file
@@ -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"<code>{Config.TRIGGER}{i}</code>" for i in Config.CMD_DICT.keys()]
|
||||
)
|
||||
await message.reply(f"<b>Available Commands:</b>\n\n{commands}", del_in=30)
|
||||
|
||||
|
||||
@bot.add_cmd(cmd="restart")
|
||||
async def restart(bot: bot, message: Message, u_resp: Message | None = None) -> None:
|
||||
reply: Message = u_resp or await message.reply("restarting....")
|
||||
if reply.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP):
|
||||
os.environ["RESTART_MSG"] = str(reply.id)
|
||||
os.environ["RESTART_CHAT"] = str(reply.chat.id)
|
||||
await bot.restart(hard="-h" in message.flags)
|
||||
|
||||
|
||||
@bot.add_cmd(cmd="repo")
|
||||
async def sauce(bot: bot, message: Message) -> None:
|
||||
await bot.send_message(
|
||||
chat_id=message.chat.id,
|
||||
text=f"<a href='{Config.UPSTREAM_REPO}'>Plain-UB.</a>",
|
||||
reply_to_message_id=message.reply_id or message.id,
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
@bot.add_cmd(cmd="update")
|
||||
async def updater(bot: bot, message: Message) -> None | Message:
|
||||
reply: Message = await message.reply("Checking for Updates....")
|
||||
repo: Repo = Repo()
|
||||
repo.git.fetch()
|
||||
commits: str = ""
|
||||
limit: int = 0
|
||||
for commit in repo.iter_commits("HEAD..origin/main"):
|
||||
commits += f"""
|
||||
<b>#{commit.count()}</b> <a href='{Config.UPSTREAM_REPO}/commit/{commit}'>{commit.summary}</a> By <i>{commit.author}</i>
|
||||
"""
|
||||
limit += 1
|
||||
if limit > 50:
|
||||
break
|
||||
if not commits:
|
||||
return await reply.edit("Already Up To Date.", del_in=5)
|
||||
if "-pull" not in message.flags:
|
||||
return await reply.edit(
|
||||
f"<b>Update Available:</b>\n\n{commits}", disable_web_page_preview=True
|
||||
)
|
||||
repo.git.reset("--hard")
|
||||
repo.git.pull(Config.UPSTREAM_REPO, "--rebase=true")
|
||||
await asyncio.gather(
|
||||
bot.log(text=f"#Updater\nPulled:\n\n{commits}", disable_web_page_preview=True),
|
||||
reply.edit("<b>Update Found</b>\n<i>Pulling....</i>"),
|
||||
)
|
||||
await restart(bot, message, reply)
|
||||
77
app/utils/aiohttp_tools.py
Normal file
77
app/utils/aiohttp_tools.py
Normal file
@@ -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)
|
||||
21
app/utils/db_utils.py
Normal file
21
app/utils/db_utils.py
Normal file
@@ -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
|
||||
7
app/utils/helpers.py
Normal file
7
app/utils/helpers.py
Normal file
@@ -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()
|
||||
47
app/utils/shell.py
Normal file
47
app/utils/shell.py
Normal file
@@ -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
|
||||
9
docker_start_cmd
Normal file
9
docker_start_cmd
Normal file
@@ -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
|
||||
10
req.txt
Normal file
10
req.txt
Normal file
@@ -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
|
||||
18
run
Executable file
18
run
Executable file
@@ -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
|
||||
19
sample-config.env
Normal file
19
sample-config.env
Normal file
@@ -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"
|
||||
Reference in New Issue
Block a user