632 lines
20 KiB
Python
632 lines
20 KiB
Python
# Ultroid - UserBot
|
|
# Copyright (C) 2021-2025 TeamUltroid
|
|
#
|
|
# This file is a part of < https://github.com/TeamUltroid/Ultroid/ >
|
|
# PLease read the GNU Affero General Public License in
|
|
# <https://github.com/TeamUltroid/pyUltroid/blob/main/LICENSE>.
|
|
|
|
import asyncio
|
|
import math
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
from traceback import format_exc
|
|
from urllib.parse import unquote
|
|
from urllib.request import urlretrieve
|
|
|
|
from .. import run_as_module
|
|
|
|
if run_as_module:
|
|
from .. import udB
|
|
from ..configs import Var
|
|
|
|
|
|
try:
|
|
from aiohttp import ClientSession as aiohttp_client
|
|
except ImportError:
|
|
aiohttp_client = None
|
|
try:
|
|
import requests
|
|
except ImportError:
|
|
requests = None
|
|
|
|
try:
|
|
import heroku3
|
|
except ImportError:
|
|
heroku3 = None
|
|
|
|
try:
|
|
from git import Repo
|
|
from git.exc import GitCommandError, InvalidGitRepositoryError, NoSuchPathError
|
|
except ImportError:
|
|
Repo = None
|
|
|
|
|
|
import asyncio
|
|
import multiprocessing
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from functools import partial, wraps
|
|
|
|
from telethon.helpers import _maybe_await
|
|
from telethon.tl import types
|
|
from telethon.utils import get_display_name
|
|
|
|
from .._misc import CMD_HELP
|
|
from .._misc._wrappers import eod, eor
|
|
from ..exceptions import DependencyMissingError
|
|
from . import *
|
|
|
|
if run_as_module:
|
|
from ..dB._core import ADDONS, HELP, LIST, LOADED
|
|
|
|
from ..version import ultroid_version
|
|
from .FastTelethon import download_file as downloadable
|
|
from .FastTelethon import upload_file as uploadable
|
|
|
|
|
|
def run_async(function):
|
|
@wraps(function)
|
|
async def wrapper(*args, **kwargs):
|
|
return await asyncio.get_event_loop().run_in_executor(
|
|
ThreadPoolExecutor(max_workers=multiprocessing.cpu_count() * 5),
|
|
partial(function, *args, **kwargs),
|
|
)
|
|
|
|
return wrapper
|
|
|
|
|
|
# ~~~~~~~~~~~~~~~~~~~~ small funcs ~~~~~~~~~~~~~~~~~~~~ #
|
|
|
|
|
|
def make_mention(user, custom=None):
|
|
if user.username:
|
|
return f"@{user.username}"
|
|
return inline_mention(user, custom=custom)
|
|
|
|
|
|
def inline_mention(user, custom=None, html=False):
|
|
mention_text = get_display_name(user) or "Deleted Account" if not custom else custom
|
|
if isinstance(user, types.User):
|
|
if html:
|
|
return f"<a href=tg://user?id={user.id}>{mention_text}</a>"
|
|
return f"[{mention_text}](tg://user?id={user.id})"
|
|
if isinstance(user, types.Channel) and user.username:
|
|
if html:
|
|
return f"<a href=https://t.me/{user.username}>{mention_text}</a>"
|
|
return f"[{mention_text}](https://t.me/{user.username})"
|
|
return mention_text
|
|
|
|
|
|
# ----------------- Load \\ Unloader ---------------- #
|
|
|
|
|
|
def un_plug(shortname):
|
|
from .. import asst, ultroid_bot
|
|
|
|
try:
|
|
all_func = LOADED[shortname]
|
|
for client in [ultroid_bot, asst]:
|
|
for x, _ in client.list_event_handlers():
|
|
if x in all_func:
|
|
client.remove_event_handler(x)
|
|
del LOADED[shortname]
|
|
del LIST[shortname]
|
|
ADDONS.remove(shortname)
|
|
except (ValueError, KeyError):
|
|
name = f"addons.{shortname}"
|
|
for client in [ultroid_bot, asst]:
|
|
for i in reversed(range(len(client._event_builders))):
|
|
ev, cb = client._event_builders[i]
|
|
if cb.__module__ == name:
|
|
del client._event_builders[i]
|
|
try:
|
|
del LOADED[shortname]
|
|
del LIST[shortname]
|
|
ADDONS.remove(shortname)
|
|
except KeyError:
|
|
pass
|
|
|
|
|
|
if run_as_module:
|
|
|
|
async def safeinstall(event):
|
|
from .. import HNDLR
|
|
from ..startup.utils import load_addons
|
|
|
|
if not event.reply_to:
|
|
return await eod(
|
|
event, f"Please use `{HNDLR}install` as reply to a .py file."
|
|
)
|
|
ok = await eor(event, "`Installing...`")
|
|
reply = await event.get_reply_message()
|
|
if not (
|
|
reply.media
|
|
and hasattr(reply.media, "document")
|
|
and reply.file.name
|
|
and reply.file.name.endswith(".py")
|
|
):
|
|
return await eod(ok, "`Please reply to any python plugin`")
|
|
plug = reply.file.name.replace(".py", "")
|
|
if plug in list(LOADED):
|
|
return await eod(ok, f"Plugin `{plug}` is already installed.")
|
|
sm = reply.file.name.replace("_", "-").replace("|", "-")
|
|
dl = await reply.download_media(f"addons/{sm}")
|
|
if event.text[9:] != "f":
|
|
read = open(dl).read()
|
|
for dan in KEEP_SAFE().All:
|
|
if re.search(dan, read):
|
|
os.remove(dl)
|
|
return await ok.edit(
|
|
f"**Installation Aborted.**\n**Reason:** Occurance of `{dan}` in `{reply.file.name}`.\n\nIf you trust the provider and/or know what you're doing, use `{HNDLR}install f` to force install.",
|
|
)
|
|
try:
|
|
load_addons(dl) # dl.split("/")[-1].replace(".py", ""))
|
|
except BaseException:
|
|
os.remove(dl)
|
|
return await eor(ok, f"**ERROR**\n\n`{format_exc()}`", time=30)
|
|
plug = sm.replace(".py", "")
|
|
if plug in HELP:
|
|
output = "**Plugin** - `{}`\n".format(plug)
|
|
for i in HELP[plug]:
|
|
output += i
|
|
output += "\n© @TeamUltroid"
|
|
await eod(ok, f"✓ `Ultroid - Installed`: `{plug}` ✓\n\n{output}")
|
|
elif plug in CMD_HELP:
|
|
output = f"Plugin Name-{plug}\n\n✘ Commands Available-\n\n"
|
|
output += str(CMD_HELP[plug])
|
|
await eod(ok, f"✓ `Ultroid - Installed`: `{plug}` ✓\n\n{output}")
|
|
else:
|
|
try:
|
|
x = f"Plugin Name-{plug}\n\n✘ Commands Available-\n\n"
|
|
for d in LIST[plug]:
|
|
x += HNDLR + d + "\n"
|
|
await eod(ok, f"✓ `Ultroid - Installed`: `{plug}` ✓\n\n`{x}`")
|
|
except BaseException:
|
|
await eod(ok, f"✓ `Ultroid - Installed`: `{plug}` ✓")
|
|
|
|
async def heroku_logs(event):
|
|
|
|
from .. import LOGS
|
|
|
|
xx = await eor(event, "`Processing...`")
|
|
if not (Var.HEROKU_API and Var.HEROKU_APP_NAME):
|
|
return await xx.edit(
|
|
"Please set `HEROKU_APP_NAME` and `HEROKU_API` in vars."
|
|
)
|
|
try:
|
|
app = (heroku3.from_key(Var.HEROKU_API)).app(Var.HEROKU_APP_NAME)
|
|
except BaseException as se:
|
|
LOGS.info(se)
|
|
return await xx.edit(
|
|
"`HEROKU_API` and `HEROKU_APP_NAME` is wrong! Kindly re-check in config vars."
|
|
)
|
|
await xx.edit("`Downloading Logs...`")
|
|
ok = app.get_log()
|
|
with open("ultroid-heroku.log", "w") as log:
|
|
log.write(ok)
|
|
await event.client.send_file(
|
|
event.chat_id,
|
|
file="ultroid-heroku.log",
|
|
thumb=ULTConfig.thumb,
|
|
caption="**Ultroid Heroku Logs.**",
|
|
)
|
|
|
|
os.remove("ultroid-heroku.log")
|
|
await xx.delete()
|
|
|
|
async def def_logs(ult, file):
|
|
await ult.respond(
|
|
"**Ultroid Logs.**",
|
|
file=file,
|
|
thumb=ULTConfig.thumb,
|
|
)
|
|
|
|
async def updateme_requirements():
|
|
"""Update requirements.."""
|
|
await bash(
|
|
f"{sys.executable} -m pip install --no-cache-dir -r requirements.txt"
|
|
)
|
|
|
|
@run_async
|
|
def gen_chlog(repo, diff):
|
|
"""Generate Changelogs..."""
|
|
UPSTREAM_REPO_URL = (
|
|
udB.get_key("UPSTREAM_REPO") or repo.remotes[0].config_reader.get("url")
|
|
).replace(".git", "")
|
|
ac_br = repo.active_branch.name
|
|
ch_log = tldr_log = ""
|
|
ch = f"<b>Ultroid {ultroid_version} updates for <a href={UPSTREAM_REPO_URL}/tree/{ac_br}>[{ac_br}]</a>:</b>"
|
|
ch_tl = f"Ultroid {ultroid_version} updates for {ac_br}:"
|
|
d_form = "%d/%m/%y || %H:%M"
|
|
for c in repo.iter_commits(diff):
|
|
ch_log += f"\n\n💬 <b>{c.count()}</b> 🗓 <b>[{c.committed_datetime.strftime(d_form)}]</b>\n<b><a href={UPSTREAM_REPO_URL.rstrip('/')}/commit/{c}>[{c.summary}]</a></b> 👨💻 <code>{c.author}</code>"
|
|
tldr_log += f"\n\n💬 {c.count()} 🗓 [{c.committed_datetime.strftime(d_form)}]\n[{c.summary}] 👨💻 {c.author}"
|
|
if ch_log:
|
|
return str(ch + ch_log), str(ch_tl + tldr_log)
|
|
return ch_log, tldr_log
|
|
|
|
|
|
# --------------------------------------------------------------------- #
|
|
|
|
|
|
async def bash(cmd, run_code=0):
|
|
"""
|
|
run any command in subprocess and get output or error."""
|
|
process = await asyncio.create_subprocess_shell(
|
|
cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
stdout, stderr = await process.communicate()
|
|
err = stderr.decode().strip() or None
|
|
out = stdout.decode().strip()
|
|
if not run_code and err:
|
|
if match := re.match(r"/bin/sh: (.*): ?(\w+): not found", err):
|
|
return out, f"{match.group(2).upper()}_NOT_FOUND"
|
|
return out, err
|
|
|
|
|
|
# ---------------------------UPDATER-------------------------------- #
|
|
# Will add in class
|
|
|
|
|
|
async def updater():
|
|
from .. import LOGS
|
|
|
|
if not Repo:
|
|
LOGS.info("Git is not installed.")
|
|
return
|
|
|
|
try:
|
|
repo = Repo()
|
|
except (NoSuchPathError, GitCommandError, InvalidGitRepositoryError) as e:
|
|
LOGS.info(f"Could not initialize git repo: {e}")
|
|
if isinstance(e, InvalidGitRepositoryError):
|
|
repo = Repo.init()
|
|
off_repo = (
|
|
udB.get_key("UPSTREAM_REPO") or "https://github.com/ThePrateekBhatia/Ultroid"
|
|
)
|
|
if "upstream" not in repo.remotes:
|
|
origin = repo.create_remote("upstream", off_repo)
|
|
origin.fetch()
|
|
repo.create_head("main", origin.refs.main)
|
|
repo.heads.main.set_tracking_branch(origin.refs.main)
|
|
repo.heads.main.checkout(True)
|
|
else:
|
|
return
|
|
|
|
ac_br = repo.active_branch.name
|
|
|
|
off_repo = udB.get_key("UPSTREAM_REPO") or repo.remotes[0].config_reader.get("url")
|
|
|
|
if "upstream" not in repo.remotes:
|
|
repo.create_remote("upstream", off_repo)
|
|
else:
|
|
repo.remote("upstream").set_url(off_repo)
|
|
|
|
ups_rem = repo.remote("upstream")
|
|
|
|
try:
|
|
ups_rem.fetch(ac_br)
|
|
except GitCommandError as e:
|
|
LOGS.info(f"Failed to fetch from upstream remote: {e}")
|
|
return False
|
|
|
|
changelog, tl_chnglog = await gen_chlog(repo, f"HEAD..upstream/{ac_br}")
|
|
return bool(changelog)
|
|
|
|
|
|
# ----------------Fast Upload/Download----------------
|
|
# @1danish_00 @new-dev0 @buddhhu
|
|
|
|
|
|
async def uploader(file, name, taime, event, msg):
|
|
with open(file, "rb") as f:
|
|
result = await uploadable(
|
|
client=event.client,
|
|
file=f,
|
|
filename=name,
|
|
progress_callback=lambda d, t: asyncio.get_event_loop().create_task(
|
|
progress(
|
|
d,
|
|
t,
|
|
event,
|
|
taime,
|
|
msg,
|
|
),
|
|
),
|
|
)
|
|
return result
|
|
|
|
|
|
async def downloader(filename, file, event, taime, msg):
|
|
with open(filename, "wb") as fk:
|
|
result = await downloadable(
|
|
client=event.client,
|
|
location=file,
|
|
out=fk,
|
|
progress_callback=lambda d, t: asyncio.get_event_loop().create_task(
|
|
progress(
|
|
d,
|
|
t,
|
|
event,
|
|
taime,
|
|
msg,
|
|
),
|
|
),
|
|
)
|
|
return result
|
|
|
|
|
|
# ~~~~~~~~~~~~~~~Async Searcher~~~~~~~~~~~~~~~
|
|
# @buddhhu
|
|
|
|
|
|
async def async_searcher(
|
|
url: str,
|
|
post: bool = False,
|
|
head: bool = False,
|
|
headers: dict = None,
|
|
evaluate=None,
|
|
object: bool = False,
|
|
re_json: bool = False,
|
|
re_content: bool = False,
|
|
*args,
|
|
**kwargs,
|
|
):
|
|
if aiohttp_client:
|
|
async with aiohttp_client(headers=headers) as client:
|
|
method = client.head if head else (client.post if post else client.get)
|
|
data = await method(url, *args, **kwargs)
|
|
if evaluate:
|
|
return await evaluate(data)
|
|
if re_json:
|
|
return await data.json()
|
|
if re_content:
|
|
return await data.read()
|
|
if head or object:
|
|
return data
|
|
return await data.text()
|
|
# elif requests:
|
|
# method = requests.head if head else (requests.post if post else requests.get)
|
|
# data = method(url, headers=headers, *args, **kwargs)
|
|
# if re_json:
|
|
# return data.json()
|
|
# if re_content:
|
|
# return data.content
|
|
# if head or object:
|
|
# return data
|
|
# return data.text
|
|
else:
|
|
raise DependencyMissingError("install 'aiohttp' to use this.")
|
|
|
|
|
|
# ~~~~~~~~~~~~~~~~~~~~DDL Downloader~~~~~~~~~~~~~~~~~~~~
|
|
# @buddhhu @new-dev0
|
|
|
|
|
|
async def download_file(link, name, validate=False):
|
|
"""for files, without progress callback with aiohttp"""
|
|
|
|
async def _download(content):
|
|
if validate and "application/json" in content.headers.get("Content-Type"):
|
|
return None, await content.json()
|
|
with open(name, "wb") as file:
|
|
file.write(await content.read())
|
|
return name, ""
|
|
|
|
return await async_searcher(link, evaluate=_download)
|
|
|
|
|
|
async def fast_download(download_url, filename=None, progress_callback=None):
|
|
if not aiohttp_client:
|
|
return await download_file(download_url, filename)[0], None
|
|
async with aiohttp_client() as session:
|
|
async with session.get(download_url, timeout=None) as response:
|
|
if not filename:
|
|
filename = unquote(download_url.rpartition("/")[-1])
|
|
total_size = int(response.headers.get("content-length", 0)) or None
|
|
downloaded_size = 0
|
|
start_time = time.time()
|
|
with open(filename, "wb") as f:
|
|
async for chunk in response.content.iter_chunked(1024):
|
|
if chunk:
|
|
f.write(chunk)
|
|
downloaded_size += len(chunk)
|
|
if progress_callback and total_size:
|
|
await _maybe_await(
|
|
progress_callback(downloaded_size, total_size)
|
|
)
|
|
return filename, time.time() - start_time
|
|
|
|
|
|
# --------------------------Media Funcs-------------------------------- #
|
|
|
|
|
|
def mediainfo(media):
|
|
xx = str((str(media)).split("(", maxsplit=1)[0])
|
|
m = ""
|
|
if xx == "MessageMediaDocument":
|
|
mim = media.document.mime_type
|
|
if mim == "application/x-tgsticker":
|
|
m = "sticker animated"
|
|
elif "image" in mim:
|
|
if mim == "image/webp":
|
|
m = "sticker"
|
|
elif mim == "image/gif":
|
|
m = "gif as doc"
|
|
else:
|
|
m = "pic as doc"
|
|
elif "video" in mim:
|
|
if "DocumentAttributeAnimated" in str(media):
|
|
m = "gif"
|
|
elif "DocumentAttributeVideo" in str(media):
|
|
i = str(media.document.attributes[0])
|
|
if "supports_streaming=True" in i:
|
|
m = "video"
|
|
m = "video as doc"
|
|
else:
|
|
m = "video"
|
|
elif "audio" in mim:
|
|
m = "audio"
|
|
else:
|
|
m = "document"
|
|
elif xx == "MessageMediaPhoto":
|
|
m = "pic"
|
|
elif xx == "MessageMediaWebPage":
|
|
m = "web"
|
|
return m
|
|
|
|
|
|
# ------------------Some Small Funcs----------------
|
|
|
|
|
|
def time_formatter(milliseconds):
|
|
minutes, seconds = divmod(int(milliseconds / 1000), 60)
|
|
hours, minutes = divmod(minutes, 60)
|
|
days, hours = divmod(hours, 24)
|
|
weeks, days = divmod(days, 7)
|
|
tmp = (
|
|
((str(weeks) + "w:") if weeks else "")
|
|
+ ((str(days) + "d:") if days else "")
|
|
+ ((str(hours) + "h:") if hours else "")
|
|
+ ((str(minutes) + "m:") if minutes else "")
|
|
+ ((str(seconds) + "s") if seconds else "")
|
|
)
|
|
if not tmp:
|
|
return "0s"
|
|
|
|
if tmp.endswith(":"):
|
|
return tmp[:-1]
|
|
return tmp
|
|
|
|
|
|
def humanbytes(size):
|
|
if not size:
|
|
return "0 B"
|
|
for unit in ["", "K", "M", "G", "T"]:
|
|
if size < 1024:
|
|
break
|
|
size /= 1024
|
|
if isinstance(size, int):
|
|
size = f"{size}{unit}B"
|
|
elif isinstance(size, float):
|
|
size = f"{size:.2f}{unit}B"
|
|
return size
|
|
|
|
|
|
def numerize(number):
|
|
if not number:
|
|
return None
|
|
unit = ""
|
|
for unit in ["", "K", "M", "B", "T"]:
|
|
if number < 1000:
|
|
break
|
|
number /= 1000
|
|
if isinstance(number, int):
|
|
number = f"{number}{unit}"
|
|
elif isinstance(number, float):
|
|
number = f"{number:.2f}{unit}"
|
|
return number
|
|
|
|
|
|
No_Flood = {}
|
|
|
|
|
|
async def progress(current, total, event, start, type_of_ps, file_name=None):
|
|
now = time.time()
|
|
if No_Flood.get(event.chat_id):
|
|
if No_Flood[event.chat_id].get(event.id):
|
|
if (now - No_Flood[event.chat_id][event.id]) < 1.1:
|
|
return
|
|
else:
|
|
No_Flood[event.chat_id].update({event.id: now})
|
|
else:
|
|
No_Flood.update({event.chat_id: {event.id: now}})
|
|
diff = time.time() - start
|
|
if round(diff % 10.00) == 0 or current == total:
|
|
percentage = current * 100 / total
|
|
speed = current / diff
|
|
time_to_completion = round((total - current) / speed) * 1000
|
|
progress_str = "`[{0}{1}] {2}%`\n\n".format(
|
|
"".join("●" for i in range(math.floor(percentage / 5))),
|
|
"".join("" for i in range(20 - math.floor(percentage / 5))),
|
|
round(percentage, 2),
|
|
)
|
|
|
|
tmp = (
|
|
progress_str
|
|
+ "`{0} of {1}`\n\n`✦ Speed: {2}/s`\n\n`✦ ETA: {3}`\n\n".format(
|
|
humanbytes(current),
|
|
humanbytes(total),
|
|
humanbytes(speed),
|
|
time_formatter(time_to_completion),
|
|
)
|
|
)
|
|
if file_name:
|
|
await event.edit(
|
|
"`✦ {}`\n\n`File Name: {}`\n\n{}".format(type_of_ps, file_name, tmp)
|
|
)
|
|
else:
|
|
await event.edit("`✦ {}`\n\n{}".format(type_of_ps, tmp))
|
|
|
|
|
|
# ------------------System\\Heroku stuff----------------
|
|
# @xditya @sppidy @techierror
|
|
|
|
|
|
async def restart(ult=None):
|
|
if Var.HEROKU_APP_NAME and Var.HEROKU_API:
|
|
try:
|
|
Heroku = heroku3.from_key(Var.HEROKU_API)
|
|
app = Heroku.apps()[Var.HEROKU_APP_NAME]
|
|
if ult:
|
|
await ult.edit("`Restarting your app, please wait for a minute!`")
|
|
app.restart()
|
|
except BaseException as er:
|
|
if ult:
|
|
return await eor(
|
|
ult,
|
|
"`HEROKU_API` or `HEROKU_APP_NAME` is wrong! Kindly re-check in config vars.",
|
|
)
|
|
LOGS.exception(er)
|
|
else:
|
|
if len(sys.argv) == 1:
|
|
os.execl(sys.executable, sys.executable, "-m", "pyUltroid")
|
|
else:
|
|
os.execl(
|
|
sys.executable,
|
|
sys.executable,
|
|
"-m",
|
|
"pyUltroid",
|
|
sys.argv[1],
|
|
sys.argv[2],
|
|
sys.argv[3],
|
|
sys.argv[4],
|
|
sys.argv[5],
|
|
sys.argv[6],
|
|
)
|
|
|
|
|
|
async def shutdown(ult):
|
|
from .. import HOSTED_ON, LOGS
|
|
|
|
ult = await eor(ult, "Shutting Down")
|
|
if HOSTED_ON == "heroku":
|
|
if not (Var.HEROKU_APP_NAME and Var.HEROKU_API):
|
|
return await ult.edit("Please Fill `HEROKU_APP_NAME` and `HEROKU_API`")
|
|
dynotype = os.getenv("DYNO").split(".")[0]
|
|
try:
|
|
Heroku = heroku3.from_key(Var.HEROKU_API)
|
|
app = Heroku.apps()[Var.HEROKU_APP_NAME]
|
|
await ult.edit("`Shutting Down your app, please wait for a minute!`")
|
|
app.process_formation()[dynotype].scale(0)
|
|
except BaseException as e:
|
|
LOGS.exception(e)
|
|
return await ult.edit(
|
|
"`HEROKU_API` and `HEROKU_APP_NAME` is wrong! Kindly re-check in config vars."
|
|
)
|
|
else:
|
|
sys.exit()
|