Initial Commit.

This commit is contained in:
thedragonsinn
2023-09-25 18:15:38 +05:30
commit 32db859d7e
24 changed files with 1066 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
config*.env
*session*
venv/
__pycache__
.idea/

22
Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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)

View 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)

View 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
View 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()

View 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
View 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
View 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
View 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
View 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
View 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)

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"