`
+ Kang the sticker (add to your pack).
+"""
+
+import contextlib
+import os
+import random, string
+from secrets import token_hex
+
+from telethon import errors
+from telethon.errors.rpcerrorlist import StickersetInvalidError
+from telethon.tl.functions.messages import GetStickerSetRequest as GetSticker
+from telethon.tl.functions.messages import UploadMediaRequest
+from telethon.tl.functions.stickers import AddStickerToSetRequest as AddSticker
+from telethon.tl.functions.stickers import CreateStickerSetRequest
+from telethon.tl.types import (
+ DocumentAttributeSticker,
+ InputPeerSelf,
+ InputStickerSetEmpty,
+)
+from telethon.errors.rpcerrorlist import ChatSendInlineForbiddenError
+from telethon.tl.types import InputStickerSetItem as SetItem
+from telethon.tl.types import InputStickerSetShortName, User
+from telethon.utils import get_display_name, get_extension, get_input_document
+from telethon.errors import PeerIdInvalidError
+
+from . import LOGS, asst, fetch, udB, ultroid_cmd, get_string,resize_photo_sticker,quotly
+
+async def packExists(packId):
+ source = await fetch(f"https://t.me/addstickers/{packId}")
+ return (
+ not b"""
+ A Telegram user has created the Sticker Set.
+
"""
+ in source
+ )
+
+async def GetUniquePackName():
+ packName = f"{random.choice(string.ascii_lowercase)}{token_hex(random.randint(4, 8))}_by_{asst.me.username}"
+ return await GetUniquePackName() if await packExists(packName) else packName
+
+
+# TODO: simplify if possible
+
+def getName(sender, packType: str):
+ title = f"{get_display_name(sender)}'s Kang Pack"
+ if packType != "static":
+ title += f" ({packType.capitalize()})"
+ return title
+
+async def AddToNewPack(file, emoji, sender_id, title: str):
+ sn = await GetUniquePackName()
+ return await asst(
+ CreateStickerSetRequest(
+ user_id=sender_id,
+ title=title,
+ short_name=sn,
+ stickers=[SetItem(file, emoji=emoji)],
+ software="@TeamUltroid",
+ )
+ )
+
+async def inline_query_fallback(ult):
+ try:
+ result = await ult.client.inline_query(asst.me.username, "startbot")
+ if result:
+ await result[0].click(ult.chat_id, hide_via=True)
+ except (ChatSendInlineForbiddenError):
+ await ult.eor(
+ f"Inline mode is disabled in this chat.\n\n"
+ f"To create or manage your sticker pack, you need to start the assistant bot first.\n\n"
+ f"Click the button below to start it:\n"
+ f"[Start Bot](https://t.me/{asst.me.username})",
+ parse_mode="md"
+ )
+ return
+
+@ultroid_cmd(pattern="kang", manager=True)
+async def kang_func(ult):
+ """kang (reply message)
+ Create sticker and add to pack"""
+ sender = await ult.get_sender()
+ if not isinstance(sender, User):
+ return
+ sender_id = sender.id
+ if not ult.is_reply:
+ return await ult.eor("`Reply to a message..`", time=5)
+ try:
+ emoji = ult.text.split(maxsplit=1)[1]
+ except IndexError:
+ emoji = None
+ reply = await ult.get_reply_message()
+ ult = await ult.eor(get_string("com_1"))
+ type_, dl = "static", None
+ if reply.sticker:
+ file = get_input_document(reply.sticker)
+ if not emoji:
+ emoji = reply.file.emoji
+ name = reply.file.name
+ ext = get_extension(reply.media)
+ attr = list(
+ filter(
+ lambda prop: isinstance(prop, DocumentAttributeSticker),
+ reply.document.attributes,
+ )
+ )
+ inPack = attr and not isinstance(attr[0].stickerset, InputStickerSetEmpty)
+ with contextlib.suppress(KeyError):
+ type_ = {".webm": "video", ".tgs": "animated"}[ext]
+ if type_ or not inPack:
+ dl = await reply.download_media()
+ elif reply.photo:
+ dl = await reply.download_media()
+ name = "sticker.webp"
+ image = resize_photo_sticker(dl)
+ image.save(name, "WEBP")
+ try:
+ os.remove(dl)
+ except:
+ pass
+ dl = name
+ elif reply.text:
+ try:
+ reply = await ult.get_reply_message()
+ replied_to = await reply.get_reply_message()
+ sender_user = await ult.client.get_entity(reply.sender_id)
+ quotly_file = await quotly.create_quotly(
+ reply, bg="black", reply=replied_to, sender=sender_user)
+ except Exception as er:
+ return await ult.edit(f"Quotly error: {er}")
+ message = await reply.reply("Quotly by Ultroid", file=quotly_file)
+ dl = quotly_file
+ else:
+ return await ult.eor("`Reply to sticker or text to add it in your pack...`")
+ if not emoji:
+ emoji = "🏵"
+ if dl:
+ upl = await asst.upload_file(dl)
+ file = get_input_document(await asst(UploadMediaRequest(InputPeerSelf(), upl)))
+ try:
+ os.remove(dl)
+ except:
+ pass
+ get_ = udB.get_key("STICKERS") or {}
+ title = getName(sender, type_)
+ if not get_.get(sender_id) or not get_.get(sender_id, {}).get(type_):
+ try:
+ pack = await AddToNewPack(file, emoji, sender.id, title)
+ except (ValueError, PeerIdInvalidError) as e:
+ await inline_query_fallback(ult)
+ return
+ except Exception as er:
+ return await ult.eor(str(er))
+ sn = pack.set.short_name
+ if not get_.get(sender_id):
+ get_.update({sender_id: {type_: [sn]}})
+ else:
+ get_[sender_id].update({type_: [sn]})
+ udB.set_key("STICKERS", get_)
+ return await ult.edit(
+ f"**Kanged Successfully!\nEmoji :** {emoji}\n**Link :** [Click Here](https://t.me/addstickers/{sn})",
+ link_preview=False
+ )
+ name = get_[sender_id][type_][-1]
+ try:
+ await asst(GetSticker(InputStickerSetShortName(name), hash=0))
+ except StickersetInvalidError:
+ get_[sender_id][type_].remove(name)
+ try:
+ await asst(
+ AddSticker(InputStickerSetShortName(name), SetItem(file, emoji=emoji))
+ )
+ except (errors.StickerpackStickersTooMuchError, errors.StickersTooMuchError):
+ try:
+ pack = await AddToNewPack(file, emoji, sender.id, title)
+ sn = pack.set.short_name
+ except (ValueError, PeerIdInvalidError) as e:
+ await inline_query_fallback(ult)
+ return
+ except Exception as er:
+ return await ult.eor(str(er))
+ get_[sender_id][type_].append(pack.set.short_name)
+ udB.set_key("STICKERS", get_)
+ return await ult.edit(
+ f"**Created New Kang Pack!\nEmoji :** {emoji}\n**Link :** [Click Here](https://t.me/addstickers/{sn})",
+ link_preview=False
+ )
+ except Exception as er:
+ LOGS.exception(er)
+ return await ult.edit(str(er))
+ await ult.edit(
+ f"Sticker Added to Pack Successfully\n**Link :** [Click Here](https://t.me/addstickers/{name})",
+ link_preview=False
+ )
+
+
+@ultroid_cmd(pattern="listpack", manager=True)
+async def do_magic(ult):
+ """Get list of sticker packs."""
+ ko = udB.get_key("STICKERS") or {}
+ if not ko.get(ult.sender_id):
+ return await ult.reply("No Sticker Pack Found!")
+ al_ = []
+ ul = ko[ult.sender_id]
+ for _ in ul.keys():
+ al_.extend(ul[_])
+ msg = "• **Stickers Owned by You!**\n\n"
+ for _ in al_:
+ try:
+ pack = await ult.client(GetSticker(InputStickerSetShortName(_), hash=0))
+ msg += f"• [{pack.set.title}](https://t.me/addstickers/{_})\n"
+ except StickersetInvalidError:
+ for type_ in ["animated", "video", "static"]:
+ if ul.get(type_) and _ in ul[type_]:
+ ul[type_].remove(_)
+ udB.set_key("STICKERS", ko)
+ await ult.reply(msg)
diff --git a/plugins/stickertools.py b/plugins/stickertools.py
deleted file mode 100644
index 95839b2da7..0000000000
--- a/plugins/stickertools.py
+++ /dev/null
@@ -1,531 +0,0 @@
-# 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
-# .
-"""
-❍ Commands Available -
-
-• `{i}destroy `
- To destroy the sticker.
-
-• `{i}tiny `
- To create Tiny stickers.
-
-• `{i}kang `
- Kang the sticker (add to your pack).
-
-• `{i}packkang `
- Kang the Complete sticker set (with custom name).
-
-• `{i}round `
- To extract round sticker.
-"""
-import glob
-import io
-import os
-import random
-from os import remove
-
-try:
- import cv2
-except ImportError:
- cv2 = None
-try:
- import numpy as np
-except ImportError:
- np = None
-try:
- from PIL import Image, ImageDraw
-except ImportError:
- pass
-
-from telethon.errors import PeerIdInvalidError, YouBlockedUserError
-from telethon.tl.functions.messages import UploadMediaRequest
-from telethon.tl.types import (
- DocumentAttributeFilename,
- DocumentAttributeSticker,
- InputPeerSelf,
-)
-from telethon.utils import get_input_document
-
-from . import (
- KANGING_STR,
- LOGS,
- asst,
- async_searcher,
- bash,
- con,
- functions,
- get_string,
- inline_mention,
- mediainfo,
- ultroid_cmd,
- quotly,
- types,
- udB,
-)
-
-
-@ultroid_cmd(pattern="packkang")
-async def pack_kangish(_):
- _e = await _.get_reply_message()
- local = None
- try:
- cmdtext = _.text.split(maxsplit=1)[1]
- except IndexError:
- cmdtext = None
- if cmdtext and os.path.isdir(cmdtext):
- local = True
- elif not (_e and _e.sticker and _e.file.mime_type == "image/webp"):
- return await _.eor(get_string("sts_4"))
- msg = await _.eor(get_string("com_1"))
- _packname = cmdtext or f"Ultroid Kang Pack By {_.sender_id}"
- typee = None
- if not local:
- _id = _e.media.document.attributes[1].stickerset.id
- _hash = _e.media.document.attributes[1].stickerset.access_hash
- _get_stiks = await _.client(
- functions.messages.GetStickerSetRequest(
- stickerset=types.InputStickerSetID(id=_id, access_hash=_hash), hash=0
- )
- )
- docs = _get_stiks.documents
- else:
- docs = []
- files = glob.glob(f"{cmdtext}/*")
- exte = files[-1]
- if exte.endswith(".tgs"):
- typee = "anim"
- elif exte.endswith(".webm"):
- typee = "vid"
- count = 0
- for file in files:
- if file.endswith((".tgs", ".webm")):
- count += 1
- upl = await asst.upload_file(file)
- docs.append(await asst(UploadMediaRequest(InputPeerSelf(), upl)))
- if count % 5 == 0:
- await msg.edit(f"`Uploaded {count} files.`")
-
- stiks = []
- for i in docs:
- x = get_input_document(i)
- stiks.append(
- types.InputStickerSetItem(
- document=x,
- emoji=(
- random.choice(["😐", "👍", "😂"])
- if local
- else (i.attributes[1]).alt
- ),
- )
- )
- try:
- short_name = "ult_" + _packname.replace(" ", "_") + str(_.id)
- _r_e_s = await asst(
- functions.stickers.CreateStickerSetRequest(
- user_id=_.sender_id,
- title=_packname,
- short_name=f"{short_name}_by_{asst.me.username}",
- animated=typee == "anim",
- videos=typee == "vid",
- stickers=stiks,
- )
- )
- except PeerIdInvalidError:
- return await msg.eor(
- f"Hey {inline_mention(_.sender)} send `/start` to @{asst.me.username} and later try this command again.."
- )
- except BaseException as er:
- LOGS.exception(er)
- return await msg.eor(str(er))
- await msg.eor(
- get_string("sts_5").format(f"https://t.me/addstickers/{_r_e_s.set.short_name}"),
- )
-
-
-@ultroid_cmd(
- pattern="kang",
-)
-async def hehe(args):
- ultroid_bot = args.client
- xx = await args.eor(get_string("com_1"))
- user = ultroid_bot.me
- username = user.username
- username = f"@{username}" if username else user.first_name
- message = await args.get_reply_message()
- photo = None
- is_anim, is_vid = False, False
- emoji = None
- if not message:
- return await xx.eor(get_string("sts_6"))
- if message.photo:
- photo = io.BytesIO()
- photo = await ultroid_bot.download_media(message.photo, photo)
- elif message.file and "image" in message.file.mime_type.split("/"):
- photo = io.BytesIO()
- await ultroid_bot.download_file(message.media.document, photo)
- if (
- DocumentAttributeFilename(file_name="sticker.webp")
- in message.media.document.attributes
- ):
- emoji = message.media.document.attributes[1].alt
-
- elif message.file and "video" in message.file.mime_type.split("/"):
- xy = await message.download_media()
- if (message.file.duration or 0) <= 10:
- is_vid = True
- photo = await con.create_webm(xy)
- else:
- y = cv2.VideoCapture(xy)
- heh, lol = y.read()
- cv2.imwrite("ult.webp", lol)
- photo = "ult.webp"
- elif message.file and "tgsticker" in message.file.mime_type:
- await ultroid_bot.download_file(
- message.media.document,
- "AnimatedSticker.tgs",
- )
- attributes = message.media.document.attributes
- for attribute in attributes:
- if isinstance(attribute, DocumentAttributeSticker):
- emoji = attribute.alt
- is_anim = True
- photo = 1
- elif message.message:
- photo = await quotly.create_quotly(message)
- else:
- return await xx.edit(get_string("com_4"))
- if not udB.get_key("language") or udB.get_key("language") == "en":
- ra = random.choice(KANGING_STR)
- else:
- ra = get_string("sts_11")
- await xx.edit(f"`{ra}`")
- if photo:
- splat = args.text.split()
- pack = 1
- if not emoji:
- emoji = "🏵"
- if len(splat) == 3:
- pack = splat[2] # User sent ultroid_both
- emoji = splat[1]
- elif len(splat) == 2:
- if splat[1].isnumeric():
- pack = int(splat[1])
- else:
- emoji = splat[1]
-
- packname = f"ult_{user.id}_{pack}"
- packnick = f"{username}'s Pack {pack}"
- cmd = "/newpack"
- file = io.BytesIO()
-
- if is_vid:
- packname += "_vid"
- packnick += " (Video)"
- cmd = "/newvideo"
- elif is_anim:
- packname += "_anim"
- packnick += " (Animated)"
- cmd = "/newanimated"
- else:
- image = con.resize_photo_sticker(photo)
- file.name = "sticker.png"
- image.save(file, "PNG")
-
- response = await async_searcher(f"http://t.me/addstickers/{packname}")
- htmlstr = response.split("\n")
-
- if (
- " A Telegram user has created the Sticker Set."
- not in htmlstr
- ):
- async with ultroid_bot.conversation("@Stickers") as conv:
- try:
- await conv.send_message("/addsticker")
- except YouBlockedUserError:
- LOGS.info("Unblocking @Stickers for kang...")
- await ultroid_bot(functions.contacts.UnblockRequest("stickers"))
- await conv.send_message("/addsticker")
- await conv.get_response()
- await conv.send_message(packname)
- x = await conv.get_response()
- if x.text.startswith("Alright! Now send me the video sticker."):
- await conv.send_file(photo, force_document=True)
- x = await conv.get_response()
- t = "50" if (is_anim or is_vid) else "120"
- while t in x.message:
- pack += 1
- packname = f"ult_{user.id}_{pack}"
- packnick = f"{username}'s Pack {pack}"
- if is_anim:
- packname += "_anim"
- packnick += " (Animated)"
- elif is_vid:
- packnick += " (Video)"
- packname += "_vid"
- await xx.edit(get_string("sts_13").format(pack))
- await conv.send_message("/addsticker")
- await conv.get_response()
- await conv.send_message(packname)
- x = await conv.get_response()
- if x.text.startswith("Alright! Now send me the video sticker."):
- await conv.send_file(photo, force_document=True)
- x = await conv.get_response()
- if x.text in ["Invalid pack selected.", "Invalid set selected."]:
- await conv.send_message(cmd)
- await conv.get_response()
- await conv.send_message(packnick)
- await conv.get_response()
- if is_anim:
- await conv.send_file("AnimatedSticker.tgs")
- remove("AnimatedSticker.tgs")
- else:
- if is_vid:
- file = photo
- else:
- file.seek(0)
- await conv.send_file(file, force_document=True)
- await conv.get_response()
- await conv.send_message(emoji)
- await conv.get_response()
- await conv.send_message("/publish")
- if is_anim:
- await conv.get_response()
- await conv.send_message(f"<{packnick}>")
- await conv.get_response()
- await conv.send_message("/skip")
- await conv.get_response()
- await conv.send_message(packname)
- await conv.get_response()
- await xx.edit(
- get_string("sts_7").format(packname),
- parse_mode="md",
- )
- return
- if is_anim:
- await conv.send_file("AnimatedSticker.tgs")
- remove("AnimatedSticker.tgs")
- elif "send me an emoji" not in x.message:
- if is_vid:
- file = photo
- else:
- file.seek(0)
- await conv.send_file(file, force_document=True)
- rsp = await conv.get_response()
- if "Sorry, the file type is invalid." in rsp.text:
- await xx.edit(
- get_string("sts_8"),
- )
- return
- await conv.send_message(emoji)
- await conv.get_response()
- await conv.send_message("/done")
- await conv.get_response()
- await ultroid_bot.send_read_acknowledge(conv.chat_id)
- else:
- await xx.edit("`Brewing a new Pack...`")
- async with ultroid_bot.conversation("Stickers") as conv:
- await conv.send_message(cmd)
- await conv.get_response()
- await conv.send_message(packnick)
- await conv.get_response()
- if is_anim:
- await conv.send_file("AnimatedSticker.tgs")
- remove("AnimatedSticker.tgs")
- else:
- if is_vid:
- file = photo
- else:
- file.seek(0)
- await conv.send_file(file, force_document=True)
- rsp = await conv.get_response()
- if "Sorry, the file type is invalid." in rsp.text:
- await xx.edit(
- get_string("sts_8"),
- )
- return
- await conv.send_message(emoji)
- await conv.get_response()
- await conv.send_message("/publish")
- if is_anim:
- await conv.get_response()
- await conv.send_message(f"<{packnick}>")
-
- await conv.get_response()
- await conv.send_message("/skip")
- await conv.get_response()
- await conv.send_message(packname)
- await conv.get_response()
- await ultroid_bot.send_read_acknowledge(conv.chat_id)
- await xx.edit(
- get_string("sts_12").format(emoji, packname),
- parse_mode="md",
- )
- try:
- os.remove(photo)
- except BaseException:
- pass
- try:
- os.remove("AnimatedSticker.tgs")
- except BaseException:
- pass
- try:
- os.remove("ult.webp")
- except BaseException:
- pass
-
-
-@ultroid_cmd(
- pattern="round$",
-)
-async def ultdround(event):
- ureply = await event.get_reply_message()
- xx = await event.eor(get_string("com_1"))
- if not (ureply and (ureply.media)):
- await xx.edit(get_string("sts_10"))
- return
- ultt = await ureply.download_media()
- file = await con.convert(
- ultt,
- convert_to="png",
- allowed_formats=["jpg", "jpeg", "png"],
- outname="round",
- remove_old=True,
- )
- img = Image.open(file).convert("RGB")
- npImage = np.array(img)
- h, w = img.size
- alpha = Image.new("L", img.size, 0)
- draw = ImageDraw.Draw(alpha)
- draw.pieslice([0, 0, h, w], 0, 360, fill=255)
- npAlpha = np.array(alpha)
- npImage = np.dstack((npImage, npAlpha))
- Image.fromarray(npImage).save("ult.webp")
- await event.client.send_file(
- event.chat_id,
- "ult.webp",
- force_document=False,
- reply_to=event.reply_to_msg_id,
- )
- await xx.delete()
- os.remove(file)
- os.remove("ult.webp")
-
-
-@ultroid_cmd(
- pattern="destroy$",
-)
-async def ultdestroy(event):
- ult = await event.get_reply_message()
- if not (ult and ult.media and "animated" in mediainfo(ult.media)):
- return await event.eor(get_string("sts_2"))
- await event.client.download_media(ult, "ultroid.tgs")
- xx = await event.eor(get_string("com_1"))
- await bash("lottie_convert.py ultroid.tgs json.json")
- with open("json.json") as json:
- jsn = json.read()
- jsn = (
- jsn.replace("[100]", "[200]")
- .replace("[10]", "[40]")
- .replace("[-1]", "[-10]")
- .replace("[0]", "[15]")
- .replace("[1]", "[20]")
- .replace("[2]", "[17]")
- .replace("[3]", "[40]")
- .replace("[4]", "[37]")
- .replace("[5]", "[60]")
- .replace("[6]", "[70]")
- .replace("[7]", "[40]")
- .replace("[8]", "[37]")
- .replace("[9]", "[110]")
- )
- open("json.json", "w").write(jsn)
- file = await con.animated_sticker("json.json", "ultroid.tgs")
- if file:
- await event.client.send_file(
- event.chat_id,
- file="ultroid.tgs",
- force_document=False,
- reply_to=event.reply_to_msg_id,
- )
- await xx.delete()
- os.remove("json.json")
-
-
-@ultroid_cmd(
- pattern="tiny$",
-)
-async def ultiny(event):
- reply = await event.get_reply_message()
- if not (reply and (reply.media)):
- await event.eor(get_string("sts_10"))
- return
- xx = await event.eor(get_string("com_1"))
- ik = await reply.download_media()
- im1 = Image.open("resources/extras/ultroid_blank.png")
- if ik.endswith(".tgs"):
- await con.animated_sticker(ik, "json.json")
- with open("json.json") as json:
- jsn = json.read()
- jsn = jsn.replace("512", "2000")
- open("json.json", "w").write(jsn)
- await con.animated_sticker("json.json", "ult.tgs")
- file = "ult.tgs"
- os.remove("json.json")
- elif ik.endswith((".gif", ".webm", ".mp4")):
- iik = cv2.VideoCapture(ik)
- dani, busy = iik.read()
- cv2.imwrite("i.png", busy)
- fil = "i.png"
- im = Image.open(fil)
- z, d = im.size
- if z == d:
- xxx, yyy = 200, 200
- else:
- t = z + d
- a = z / t
- b = d / t
- aa = (a * 100) - 50
- bb = (b * 100) - 50
- xxx = 200 + 5 * aa
- yyy = 200 + 5 * bb
- k = im.resize((int(xxx), int(yyy)))
- k.save("k.png", format="PNG", optimize=True)
- im2 = Image.open("k.png")
- back_im = im1.copy()
- back_im.paste(im2, (150, 0))
- back_im.save("o.webp", "WEBP", quality=95)
- file = "o.webp"
- os.remove(fil)
- os.remove("k.png")
- else:
- im = Image.open(ik)
- z, d = im.size
- if z == d:
- xxx, yyy = 200, 200
- else:
- t = z + d
- a = z / t
- b = d / t
- aa = (a * 100) - 50
- bb = (b * 100) - 50
- xxx = 200 + 5 * aa
- yyy = 200 + 5 * bb
- k = im.resize((int(xxx), int(yyy)))
- k.save("k.png", format="PNG", optimize=True)
- im2 = Image.open("k.png")
- back_im = im1.copy()
- back_im.paste(im2, (150, 0))
- back_im.save("o.webp", "WEBP", quality=95)
- file = "o.webp"
- os.remove("k.png")
- if os.path.exists(file):
- await event.client.send_file(
- event.chat_id, file, reply_to=event.reply_to_msg_id
- )
- os.remove(file)
- await xx.delete()
- os.remove(ik)
diff --git a/plugins/tag.py b/plugins/tag.py
deleted file mode 100644
index 141e5feac1..0000000000
--- a/plugins/tag.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# 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
-# .
-"""
-✘ Commands Available -
-
-• `{i}tagall`
- Tag Top 100 Members of chat.
-
-• `{i}tagadmins`
- Tag Admins of that chat.
-
-• `{i}tagowner`
- Tag Owner of that chat
-
-• `{i}tagbots`
- Tag Bots of that chat.
-
-• `{i}tagrec`
- Tag recently Active Members.
-
-• `{i}tagon`
- Tag online Members(work only if privacy off).
-
-• `{i}tagoff`
- Tag Offline Members(work only if privacy off).
-"""
-
-from telethon.tl.types import ChannelParticipantAdmin as admin
-from telethon.tl.types import ChannelParticipantCreator as owner
-from telethon.tl.types import UserStatusOffline as off
-from telethon.tl.types import UserStatusOnline as onn
-from telethon.tl.types import UserStatusRecently as rec
-
-from . import inline_mention, ultroid_cmd
-
-
-@ultroid_cmd(
- pattern="tag(on|off|all|bots|rec|admins|owner)( (.*)|$)",
- groups_only=True,
-)
-async def _(e):
- okk = e.text
- lll = e.pattern_match.group(2)
- o = 0
- nn = 0
- rece = 0
- xx = f"{lll}" if lll else ""
- lili = await e.client.get_participants(e.chat_id, limit=99)
- for bb in lili:
- x = bb.status
- y = bb.participant
- if isinstance(x, onn):
- o += 1
- if "on" in okk:
- xx += f"\n{inline_mention(bb)}"
- elif isinstance(x, off):
- nn += 1
- if "off" in okk and not bb.bot and not bb.deleted:
- xx += f"\n{inline_mention(bb)}"
- elif isinstance(x, rec):
- rece += 1
- if "rec" in okk and not bb.bot and not bb.deleted:
- xx += f"\n{inline_mention(bb)}"
- if isinstance(y, owner):
- xx += f"\n꧁{inline_mention(bb)}꧂"
- if isinstance(y, admin) and "admin" in okk and not bb.deleted:
- xx += f"\n{inline_mention(bb)}"
- if "all" in okk and not bb.bot and not bb.deleted:
- xx += f"\n{inline_mention(bb)}"
- if "bot" in okk and bb.bot:
- xx += f"\n{inline_mention(bb)}"
- await e.eor(xx)
diff --git a/plugins/tools.py b/plugins/tools.py
index 9d57810434..00b2172600 100644
--- a/plugins/tools.py
+++ b/plugins/tools.py
@@ -36,8 +36,12 @@
import glob
import io
import os
-import secrets
+import secrets, time, re, asyncio
+from telethon.tl import types, functions
+from datetime import datetime as dt, timedelta
from asyncio.exceptions import TimeoutError as AsyncTimeout
+from telethon.utils import get_display_name
+from telethon.tl.functions.contacts import UnblockRequest
try:
import cv2
@@ -62,6 +66,25 @@
from pyUltroid.fns.tools import metadata, translate
+from aiohttp.client_exceptions import InvalidURL
+from telethon.errors.rpcerrorlist import MessageNotModifiedError
+
+from pyUltroid.fns.helper import time_formatter
+from pyUltroid.fns.tools import get_chat_and_msgid, set_attributes
+
+from . import (
+ LOGS,
+ ULTConfig,
+ downloader,
+ eor,
+ fast_download,
+ get_all_files,
+ get_string,
+ progress,
+ time_formatter,
+ ultroid_cmd,
+)
+
from . import (
HNDLR,
LOGS,
@@ -368,9 +391,9 @@ async def sangmata(event):
try:
if user.isdigit():
- userinfo = await ultroid_bot.get_entity(int(user))
+ userinfo = await event.client.get_entity(int(user))
else:
- userinfo = await ultroid_bot.get_entity(user)
+ userinfo = await event.client.get_entity(user)
except ValueError:
userinfo = None
if not isinstance(userinfo, types.User):
@@ -383,7 +406,7 @@ async def sangmata(event):
try:
await conv.send_message(f"{userinfo.id}")
except YouBlockedUserError:
- await catub(unblock("SangMata_beta_bot"))
+ await event.client(UnblockRequest("SangMata_beta_bot"))
await conv.send_message(f"{userinfo.id}")
responses = []
while True:
@@ -461,3 +484,188 @@ async def webss(event):
os.remove(pic)
await xx.delete()
+
+
+
+@ultroid_cmd(
+ pattern="dl( (.*)|$)",
+)
+async def download(event):
+ match = event.pattern_match.group(1).strip()
+ xx = await event.eor(get_string("com_1"))
+
+ # Handle web links
+ if match and ("http://" in match or "https://" in match):
+ try:
+ splited = match.split(" | ")
+ link = splited[0]
+ filename = splited[1] if len(splited) > 1 else None
+ s_time = time.time()
+ try:
+ filename, d = await fast_download(
+ link,
+ filename,
+ progress_callback=lambda d, t: asyncio.get_event_loop().create_task(
+ progress(
+ d,
+ t,
+ xx,
+ s_time,
+ f"Downloading from {link}",
+ )
+ ),
+ )
+ except InvalidURL:
+ return await xx.eor("`Invalid URL provided :(`", time=5)
+ return await xx.eor(f"`{filename}` `downloaded in {time_formatter(d*1000)}.`")
+ except Exception as e:
+ LOGS.exception(e)
+ return await xx.eor(f"`Error: {str(e)}`", time=5)
+
+ # Handle Telegram links
+ if match and "t.me/" in match:
+ chat, msg = get_chat_and_msgid(match)
+ if not (chat and msg):
+ return await xx.eor(get_string("gms_1"))
+ match = ""
+ ok = await event.client.get_messages(chat, ids=msg)
+ elif event.reply_to_msg_id:
+ ok = await event.get_reply_message()
+ else:
+ return await xx.eor(get_string("cvt_3"), time=8)
+
+ if not (ok and ok.media):
+ return await xx.eor(get_string("udl_1"), time=5)
+
+ s = dt.now()
+ k = time.time()
+ if hasattr(ok.media, "document"):
+ file = ok.media.document
+ mime_type = file.mime_type
+ filename = match or ok.file.name
+ if not filename:
+ if "audio" in mime_type:
+ filename = "audio_" + dt.now().isoformat("_", "seconds") + ".ogg"
+ elif "video" in mime_type:
+ filename = "video_" + dt.now().isoformat("_", "seconds") + ".mp4"
+ try:
+ result = await downloader(
+ f"resources/downloads/{filename}",
+ file,
+ xx,
+ k,
+ f"Downloading {filename}...",
+ )
+ except MessageNotModifiedError as err:
+ return await xx.edit(str(err))
+ file_name = result.name
+ else:
+ d = "resources/downloads/"
+ file_name = await event.client.download_media(
+ ok,
+ d,
+ progress_callback=lambda d, t: asyncio.get_event_loop().create_task(
+ progress(
+ d,
+ t,
+ xx,
+ k,
+ get_string("com_5"),
+ ),
+ ),
+ )
+ e = dt.now()
+ t = time_formatter(((e - s).seconds) * 1000)
+ await xx.eor(get_string("udl_2").format(file_name, t))
+
+
+@ultroid_cmd(
+ pattern="ul( (.*)|$)",
+)
+async def _(event):
+ msg = await event.eor(get_string("com_1"))
+ match = event.pattern_match.group(1)
+ if match:
+ match = match.strip()
+ if not event.out and match == ".env":
+ return await event.reply("`You can't do this...`")
+ stream, force_doc, delete, thumb = (
+ False,
+ True,
+ False,
+ ULTConfig.thumb,
+ )
+ if "--stream" in match:
+ stream = True
+ force_doc = False
+ if "--delete" in match:
+ delete = True
+ if "--no-thumb" in match:
+ thumb = None
+ arguments = ["--stream", "--delete", "--no-thumb"]
+ if any(item in match for item in arguments):
+ match = (
+ match.replace("--stream", "")
+ .replace("--delete", "")
+ .replace("--no-thumb", "")
+ .strip()
+ )
+ if match.endswith("/"):
+ match += "*"
+ results = glob.glob(match)
+ if not results and os.path.exists(match):
+ results = [match]
+ if not results:
+ try:
+ await event.reply(file=match)
+ return await event.try_delete()
+ except Exception as er:
+ LOGS.exception(er)
+ return await msg.eor(get_string("ls1"))
+ for result in results:
+ if os.path.isdir(result):
+ c, s = 0, 0
+ for files in get_all_files(result):
+ attributes = None
+ if stream:
+ try:
+ attributes = await set_attributes(files)
+ except KeyError as er:
+ LOGS.exception(er)
+ try:
+ file, _ = await event.client.fast_uploader(
+ files, show_progress=True, event=msg, to_delete=delete
+ )
+ await event.client.send_file(
+ event.chat_id,
+ file,
+ supports_streaming=stream,
+ force_document=force_doc,
+ thumb=thumb,
+ attributes=attributes,
+ caption=f"`Uploaded` `{files}` `in {time_formatter(_*1000)}`",
+ reply_to=event.reply_to_msg_id or event,
+ )
+ s += 1
+ except (ValueError, IsADirectoryError):
+ c += 1
+ break
+ attributes = None
+ if stream:
+ try:
+ attributes = await set_attributes(result)
+ except KeyError as er:
+ LOGS.exception(er)
+ file, _ = await event.client.fast_uploader(
+ result, show_progress=True, event=msg, to_delete=delete
+ )
+ await event.client.send_file(
+ event.chat_id,
+ file,
+ supports_streaming=stream,
+ force_document=force_doc,
+ thumb=thumb,
+ attributes=attributes,
+ caption=f"`Uploaded` `{result}` `in {time_formatter(_*1000)}`",
+ )
+ await msg.try_delete()
diff --git a/plugins/twitter.py b/plugins/twitter.py
deleted file mode 100644
index 06613cee2c..0000000000
--- a/plugins/twitter.py
+++ /dev/null
@@ -1,205 +0,0 @@
-# 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
-# .
-
-"""
-✘ Commands Available -
-
-• `{i}tw `
- Tweet the text.
-
-• `{i}twr `
- Get tweet details with reply/quote/comment count.
-
-• `{i}twuser `
- Get user details of the Twitter account.
-
-• `{i}twl `
- Upload the tweet media to telegram.
-
-"""
-
-import os
-from twikit import Client
-from . import LOGS, eor, get_string, udB, ultroid_cmd
-
-# Store client globally
-twitter_client = None
-
-# Get path to cookies file
-COOKIES_FILE = "resources/auth/twitter_cookies.json"
-
-async def get_client():
- global twitter_client
- if twitter_client:
- return twitter_client
-
- if not all(udB.get_key(key) for key in ["TWITTER_USERNAME", "TWITTER_EMAIL", "TWITTER_PASSWORD"]):
- raise Exception("Set TWITTER_USERNAME, TWITTER_EMAIL and TWITTER_PASSWORD in vars first!")
-
- # Create auth directory if it doesn't exist
- os.makedirs(os.path.dirname(COOKIES_FILE), exist_ok=True)
-
- client = Client()
- await client.login(
- auth_info_1=udB.get_key("TWITTER_USERNAME"),
- auth_info_2=udB.get_key("TWITTER_EMAIL"),
- password=udB.get_key("TWITTER_PASSWORD"),
- cookies_file=COOKIES_FILE
- )
- twitter_client = client
- return client
-
-
-
-@ultroid_cmd(pattern="tw( (.*)|$)")
-async def tweet_cmd(event):
- """Post a tweet"""
- text = event.pattern_match.group(1).strip()
- if not text:
- return await event.eor("🚫 `Give some text to tweet!`")
-
- msg = await event.eor("🕊 `Tweeting...`")
- try:
- client = await get_client()
- tweet = await client.create_tweet(text=text)
- await msg.edit(f"✨ **Successfully Posted!**\n\n🔗 https://x.com/{tweet.user.screen_name}/status/{tweet.id}")
- except Exception as e:
- await msg.edit(f"❌ **Error:**\n`{str(e)}`")
-
-
-@ultroid_cmd(pattern="twdetail( (.*)|$)")
-async def twitter_details(event):
- """Get tweet details"""
- match = event.pattern_match.group(1).strip()
- if not match:
- return await event.eor("🚫 `Give tweet ID/link to get details!`")
-
- msg = await event.eor("🔍 `Getting tweet details...`")
- try:
- client = await get_client()
- from urllib.parse import urlparse
- parsed_url = urlparse(match)
- if parsed_url.hostname in ["twitter.com", "x.com"]:
- tweet_id = parsed_url.path.split("/")[-1].split("?")[0]
- else:
- tweet_id = match
-
- tweet = await client.get_tweet_by_id(tweet_id)
- text = "🐦 **Tweet Details**\n\n"
- text += f"📝 **Content:** `{tweet.text}`\n\n"
- if hasattr(tweet, "metrics"):
- text += f"❤️ **Likes:** `{tweet.metrics.likes}`\n"
- text += f"🔄 **Retweets:** `{tweet.metrics.retweets}`\n"
- text += f"💬 **Replies:** `{tweet.metrics.replies}`\n"
- text += f"👁 **Views:** `{tweet.metrics.views}`\n"
-
- await msg.edit(text)
- except Exception as e:
- await msg.edit(f"❌ **Error:**\n`{str(e)}`")
-
-
-@ultroid_cmd(pattern="twuser( (.*)|$)")
-async def twitter_user(event):
- """Get user details"""
- match = event.pattern_match.group(1).strip()
- if not match:
- return await event.eor("🚫 `Give username to get details!`")
-
- msg = await event.eor("🔍 `Getting user details...`")
- try:
- client = await get_client()
- user = await client.get_user_by_screen_name(match)
- text = "👤 **Twitter User Details**\n\n"
- text += f"📛 **Name:** `{user.name}`\n"
- text += f"🔖 **Username:** `@{user.screen_name}`\n"
- text += f"📝 **Bio:** `{user.description}`\n\n"
- text += f"👥 **Followers:** `{user.followers_count}`\n"
- text += f"👣 **Following:** `{user.following_count}`\n"
- text += f"🐦 **Total Tweets:** `{user.statuses_count}`\n"
- text += f"📍 **Location:** `{user.location or 'Not Set'}`\n"
- text += f"✅ **Verified:** `{user.verified}`\n"
-
- if user.profile_image_url:
- image_url = user.profile_image_url.replace("_normal.", ".")
- await event.client.send_file(
- event.chat_id,
- file=image_url,
- caption=text,
- force_document=False
- )
- await msg.delete()
- else:
- await msg.edit(text)
-
- except Exception as e:
- await msg.edit(f"❌ **Error:**\n`{str(e)}`")
-
-
-@ultroid_cmd(pattern="twl( (.*)|$)")
-async def twitter_media(event):
- """Download tweet media"""
- match = event.pattern_match.group(1).strip()
- if not match:
- return await event.eor("🚫 `Give tweet link to download media!`")
-
- msg = await event.eor("📥 `Downloading media...`")
- try:
- client = await get_client()
- if "twitter.com" in match or "x.com" in match:
- tweet_id = match.split("/")[-1].split("?")[0]
- else:
- tweet_id = match
-
- tweet = await client.get_tweet_by_id(tweet_id)
-
- if not hasattr(tweet, "media"):
- return await msg.edit("😕 `No media found in tweet!`")
-
- # Prepare caption with tweet text
- caption = f"🐦 **Tweet by @{tweet.user.screen_name}**\n\n"
- caption += f"{tweet.text}\n\n"
- if hasattr(tweet, "metrics"):
- caption += f"❤️ `{tweet.metrics.likes}` 🔄 `{tweet.metrics.retweets}` 💬 `{tweet.metrics.replies}`"
-
- media_count = 0
- for media in tweet.media:
- if media.type == "photo":
- await event.client.send_file(
- event.chat_id,
- media.url,
- caption=caption if media_count == 0 else None # Only add caption to first media
- )
- media_count += 1
- elif media.type == "video":
- if hasattr(media, "video_info") and isinstance(media.video_info, dict):
- variants = media.video_info.get("variants", [])
- mp4_variants = [
- v for v in variants
- if v.get("content_type") == "video/mp4" and "bitrate" in v
- ]
- if mp4_variants:
- best_video = max(mp4_variants, key=lambda x: x["bitrate"])
- video_caption = caption if media_count == 0 else "" # Only add tweet text to first media
- if video_caption:
- video_caption += f"\n🎥 Video Quality: {best_video['bitrate']/1000:.0f}kbps"
- else:
- video_caption = f"🎥 Video Quality: {best_video['bitrate']/1000:.0f}kbps"
-
- await event.client.send_file(
- event.chat_id,
- best_video["url"],
- caption=video_caption
- )
- media_count += 1
-
- if media_count > 0:
- await msg.edit(f"✅ Successfully downloaded {media_count} media items!")
- await msg.delete()
- else:
- await msg.edit("😕 `No media could be downloaded!`")
- except Exception as e:
- await msg.edit(f"❌ **Error:**\n`{str(e)}`")
diff --git a/plugins/unsplash.py b/plugins/unsplash.py
deleted file mode 100644
index 4393009fe7..0000000000
--- a/plugins/unsplash.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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
-# .
-"""
-✘ Commands Available -
-
-• {i}unsplash ;
- Unsplash Image Search.
-"""
-
-from pyUltroid.fns.misc import unsplashsearch
-
-from . import asyncio, download_file, get_string, os, ultroid_cmd
-
-
-@ultroid_cmd(pattern="unsplash( (.*)|$)")
-async def searchunsl(ult):
- match = ult.pattern_match.group(1).strip()
- if not match:
- return await ult.eor("Give me Something to Search")
- num = 5
- if ";" in match:
- num = int(match.split(";")[1])
- match = match.split(";")[0]
- tep = await ult.eor(get_string("com_1"))
- res = await unsplashsearch(match, limit=num)
- if not res:
- return await ult.eor(get_string("unspl_1"), time=5)
- CL = [download_file(rp, f"{match}-{e}.png") for e, rp in enumerate(res)]
- imgs = [z[0] for z in (await asyncio.gather(*CL)) if z]
- await ult.respond(f"Uploaded {len(imgs)} Images!", file=imgs)
- await tep.delete()
- [os.remove(img) for img in imgs]
diff --git a/plugins/utilities.py b/plugins/utilities.py
index f8b62f643f..6aa39dfa9d 100644
--- a/plugins/utilities.py
+++ b/plugins/utilities.py
@@ -55,7 +55,7 @@
import io
import os
import pathlib
-import time
+import time, math
from datetime import datetime as dt
try:
@@ -64,7 +64,6 @@
Image = None
from pyUltroid._misc._assistant import asst_cmd
-from pyUltroid.dB.gban_mute_db import is_gbanned
from pyUltroid.fns.tools import get_chat_and_msgid
from . import upload_file as uf
@@ -72,6 +71,7 @@
from telethon.errors.rpcerrorlist import ChatForwardsRestrictedError, UserBotError
from telethon.errors import MessageTooLongError
from telethon.events import NewMessage
+from telethon.tl import types, functions
from telethon.tl.custom import Dialog
from telethon.tl.functions.channels import (
GetAdminedPublicChannelsRequest,
@@ -94,6 +94,7 @@
MessageMediaPhoto,
MessageMediaDocument,
DocumentAttributeVideo,
+
)
from telethon.utils import get_peer_id
@@ -284,28 +285,23 @@ async def _(event):
pattern="info( (.*)|$)",
manager=True,
)
-async def _(event):
- if match := event.pattern_match.group(1).strip():
- try:
- user = await event.client.parse_id(match)
- except Exception as er:
- return await event.eor(str(er))
- elif event.is_reply:
- rpl = await event.get_reply_message()
- user = rpl.sender_id
- else:
- user = event.chat_id
+async def getInfo(event):
+ user = event.pattern_match.group(1).strip()
+ if not user:
+ if event.is_reply:
+ rpl = await event.get_reply_message()
+ user = rpl.sender_id
+ else:
+ user = event.chat_id
xx = await event.eor(get_string("com_1"))
try:
_ = await event.client.get_entity(user)
except Exception as er:
return await xx.edit(f"**ERROR :** {er}")
- if not isinstance(_, User):
+ if not isinstance(_, types.User):
try:
- peer = get_peer_id(_)
+ get_peer_id(_)
photo, capt = await get_chat_info(_, event)
- if is_gbanned(peer):
- capt += "\n• Is Gbanned: True"
if not photo:
return await xx.eor(capt, parse_mode="html")
await event.client.send_message(
@@ -316,7 +312,7 @@ async def _(event):
await event.eor("**ERROR ON CHATINFO**\n" + str(er))
return
try:
- full_user = (await event.client(GetFullUserRequest(user))).full_user
+ full_user: types.UserFull = (await event.client(GetFullUserRequest(user))).full_user
except Exception as er:
return await xx.edit(f"ERROR : {er}")
user = _
@@ -329,46 +325,42 @@ async def _(event):
first_name = first_name.replace("\u2060", "")
last_name = user.last_name
last_name = (
- last_name.replace("\u2060", "") if last_name else ("Last Name not found")
+ last_name.replace("\u2060", "") if last_name else (
+ "Last Name not found")
)
- user_bio = full_user.about
- if user_bio is not None:
- user_bio = html.escape(full_user.about)
+ user_bio = html.escape(full_user.about or "")
common_chats = full_user.common_chats_count
if user.photo:
dc_id = user.photo.dc_id
else:
dc_id = "Need a Profile Picture to check this"
- caption = """Exᴛʀᴀᴄᴛᴇᴅ Dᴀᴛᴀ Fʀᴏᴍ Tᴇʟᴇɢʀᴀᴍ's Dᴀᴛᴀʙᴀsᴇ
-••Tᴇʟᴇɢʀᴀᴍ ID: {}
-••Pᴇʀᴍᴀɴᴇɴᴛ Lɪɴᴋ: Click Here
-••Fɪʀsᴛ Nᴀᴍᴇ: {}
-••Sᴇᴄᴏɴᴅ Nᴀᴍᴇ: {}
-••Bɪᴏ: {}
-••Dᴄ ID: {}
-••Nᴏ. Oғ PғPs : {}
-••Is Rᴇsᴛʀɪᴄᴛᴇᴅ: {}
-••Vᴇʀɪғɪᴇᴅ: {}
-••Is Pʀᴇᴍɪᴜᴍ: {}
-••Is A Bᴏᴛ: {}
-••Gʀᴏᴜᴘs Iɴ Cᴏᴍᴍᴏɴ: {}
-""".format(
- user_id,
- user_id,
- first_name,
- last_name,
- user_bio,
- dc_id,
- user_photos,
- user.restricted,
- user.verified,
- user.premium,
- user.bot,
- common_chats,
- )
- if chk := is_gbanned(user_id):
- caption += f"""••Gʟᴏʙᴀʟʟʏ Bᴀɴɴᴇᴅ: True
-••Rᴇᴀsᴏɴ: {chk}"""
+ caption = f"""Exᴛʀᴀᴄᴛᴇᴅ Dᴀᴛᴀ Fʀᴏᴍ Tᴇʟᴇɢʀᴀᴍ's Dᴀᴛᴀʙᴀsᴇ
+••Tᴇʟᴇɢʀᴀᴍ ID: {user_id}
+••Pᴇʀᴍᴀɴᴇɴᴛ Lɪɴᴋ: Click Here
+••Fɪʀsᴛ Nᴀᴍᴇ: {first_name}"""
+ if not user.bot:
+ caption += f"\n••Sᴇᴄᴏɴᴅ Nᴀᴍᴇ: {last_name}"
+ caption += f"""\n••Bɪᴏ: {user_bio}
+••Dᴄ ID: {dc_id}"""
+ if (b_date:= full_user.birthday):
+ date = f"{b_date.day}-{b_date.month}"
+ if b_date.year:
+ date += f"-{b_date.year}"
+ caption += f"\n••Birthday : {date}"
+ if full_user.stories:
+ caption += f"\n••Stories Count : {len(full_user.stories.stories)}"
+ if user_photos:
+ caption += f"\n••Nᴏ. Oғ PғPs : {user_photos}"
+ if not user.bot:
+ caption += f"\n••Is Rᴇsᴛʀɪᴄᴛᴇᴅ: {user.restricted}"
+ caption += f"\n••Is Pʀᴇᴍɪᴜᴍ: {user.premium}"
+ caption += f"""\n••Vᴇʀɪғɪᴇᴅ: {user.verified}
+••Is A Bᴏᴛ: {user.bot}
+••Gʀᴏᴜᴘs Iɴ Cᴏᴍᴍᴏɴ: {common_chats}
+"""
+ # if chk := is_gbanned(user_id):
+ # caption += f"""••Gʟᴏʙᴀʟʟʏ Bᴀɴɴᴇᴅ: True
+ # ••Rᴇᴀsᴏɴ: {chk}"""
await event.client.send_message(
event.chat_id,
caption,
@@ -381,6 +373,148 @@ async def _(event):
await xx.delete()
+async def get_chat_info(chat, event):
+ if isinstance(chat, types.Channel):
+ chat_info = await event.client(functions.channels.GetFullChannelRequest(chat))
+ elif isinstance(chat, types.Chat):
+ chat_info = await event.client(functions.messages.GetFullChatRequest(chat))
+ else:
+ return await event.eor("`Use this for Group/Channel.`")
+ full = chat_info.full_chat
+ chat_photo = full.chat_photo
+ broadcast = getattr(chat, "broadcast", False)
+ chat_type = "Channel" if broadcast else "Group"
+ chat_title = chat.title
+ try:
+ msg_info = await event.client(
+ functions.messages.GetHistoryRequest(
+ peer=chat.id,
+ offset_id=0,
+ offset_date=None,
+ add_offset=-0,
+ limit=0,
+ max_id=0,
+ min_id=0,
+ hash=0,
+ )
+ )
+ except Exception as er:
+ msg_info = None
+ if not event.client._bot:
+ LOGS.exception(er)
+ first_msg_valid = bool(
+ msg_info and msg_info.messages and msg_info.messages[0].id == 1
+ )
+
+ creator_valid = bool(first_msg_valid and msg_info.users)
+ creator_id = msg_info.users[0].id if creator_valid else None
+ creator_firstname = (
+ msg_info.users[0].first_name
+ if creator_valid and msg_info.users[0].first_name is not None
+ else "Deleted Account"
+ )
+ creator_username = (
+ msg_info.users[0].username
+ if creator_valid and msg_info.users[0].username is not None
+ else None
+ )
+ created = msg_info.messages[0].date if first_msg_valid else None
+ if not isinstance(chat.photo, types.ChatPhotoEmpty):
+ dc_id = chat.photo.dc_id
+ else:
+ dc_id = "Null"
+
+ restricted_users = getattr(full, "banned_count", None)
+ members = getattr(full, "participants_count", chat.participants_count)
+ admins = getattr(full, "admins_count", None)
+ banned_users = getattr(full, "kicked_count", None)
+ members_online = getattr(full, "online_count", 0)
+ group_stickers = (
+ full.stickerset.title if getattr(full, "stickerset", None) else None
+ )
+ messages_viewable = msg_info.count if msg_info else None
+ messages_sent = getattr(full, "read_inbox_max_id", None)
+ messages_sent_alt = getattr(full, "read_outbox_max_id", None)
+ exp_count = getattr(full, "pts", None)
+ supergroup = "Yes" if getattr(chat, "megagroup", None) else "No"
+ creator_username = "@{}".format(
+ creator_username) if creator_username else None
+
+ if admins is None:
+ try:
+ participants_admins = await event.client(
+ functions.channels.GetParticipantsRequest(
+ channel=chat.id,
+ filter=types.ChannelParticipantsAdmins(),
+ offset=0,
+ limit=0,
+ hash=0,
+ )
+ )
+ admins = participants_admins.count if participants_admins else None
+ except Exception as e:
+ LOGS.info(f"Exception: {e}")
+ caption = "ℹ️ [CHAT INFO]\n"
+ caption += f"🆔 ID: {chat.id}\n"
+ if chat_title is not None:
+ caption += f"📛 {chat_type} name: {chat_title}\n"
+ if chat.username:
+ caption += f"🔗 Link: @{chat.username}\n"
+ else:
+ caption += f"🗳 {chat_type} type: Private\n"
+ if creator_username:
+ caption += f"🖌 Creator: {creator_username}\n"
+ elif creator_valid:
+ caption += f'🖌 Creator: {creator_firstname}\n'
+ if created:
+ caption += f"🖌 Created: {created.date().strftime('%b %d, %Y')} - {created.time()}\n"
+ else:
+ caption += f"🖌 Created: {chat.date.date().strftime('%b %d, %Y')} - {chat.date.time()} ⚠\n"
+ caption += f"🗡 Data Centre ID: {dc_id}\n"
+ if exp_count is not None:
+ chat_level = int((1 + math.sqrt(1 + 7 * exp_count / 14)) / 2)
+ caption += f"⭐️ {chat_type} level: {chat_level}\n"
+ if messages_viewable is not None:
+ caption += f"💬 Viewable messages: {messages_viewable}\n"
+ if messages_sent:
+ caption += f"💬 Messages sent: {messages_sent}\n"
+ elif messages_sent_alt:
+ caption += f"💬 Messages sent: {messages_sent_alt} ⚠\n"
+ if members is not None:
+ caption += f"👥 Members: {members}\n"
+ if admins:
+ caption += f"👮 Administrators: {admins}\n"
+ if full.bot_info:
+ caption += f"🤖 Bots: {len(full.bot_info)}\n"
+ if members_online:
+ caption += f"👀 Currently online: {members_online}\n"
+ if restricted_users is not None:
+ caption += f"🔕 Restricted users: {restricted_users}\n"
+ if banned_users:
+ caption += f"📨 Banned users: {banned_users}\n"
+ if group_stickers:
+ caption += f'📹 {chat_type} stickers: {group_stickers}\n'
+ if not broadcast:
+ if getattr(chat, "slowmode_enabled", None):
+ caption += f"👉 Slow mode: True"
+ caption += f", 🕐 {full.slowmode_seconds}s\n"
+ else:
+ caption += f"🦸♂ Supergroup: {supergroup}\n"
+ if getattr(chat, "restricted", None):
+ caption += f"🎌 Restricted: {chat.restricted}\n"
+ rist = chat.restriction_reason[0]
+ caption += f"> Platform: {rist.platform}\n"
+ caption += f"> Reason: {rist.reason}\n"
+ caption += f"> Text: {rist.text}\n\n"
+ if getattr(chat, "scam", None):
+ caption += "⚠ Scam: Yes\n"
+ if getattr(chat, "verified", None):
+ caption += f"✅ Verified by Telegram: Yes\n\n"
+ if full.about:
+ caption += f"🗒 Description: \n{full.about}\n"
+ return chat_photo, caption
+
+
@ultroid_cmd(
pattern="invite( (.*)|$)",
groups_only=True,
@@ -732,6 +866,8 @@ async def get_thumbnail(file_path, thumbnail_path):
except Exception as e:
print(f"Error extracting thumbnail: {e}")
+
+
@ultroid_cmd(pattern="getmsg( ?(.*)|$)")
async def get_restricted_msg(event):
match = event.pattern_match.group(1).strip()
@@ -743,14 +879,21 @@ async def get_restricted_msg(event):
chat, msg = get_chat_and_msgid(match)
if not (chat and msg):
return await event.eor(
- "Invalid link!\nEg: `https://t.me/TeamUltroid/3` or `https://t.me/c/1313492028/3`"
+ "Invalid link!\nExamples:\n"
+ "`https://t.me/TeamUltroid/3`\n"
+ "`https://t.me/c/1313492028/3`\n"
+ "`tg://openmessage?user_id=1234567890&message_id=1`"
)
try:
- message = await event.client.get_messages(chat, ids=msg)
+ input_entity = await event.client.get_input_entity(chat)
+ message = await event.client.get_messages(input_entity, ids=msg)
except BaseException as er:
return await event.eor(f"**ERROR**\n`{er}`")
+ if not message:
+ return await event.eor("`Message not found or may not exist.`")
+
try:
await event.client.send_message(event.chat_id, message)
await xx.try_delete()
diff --git a/plugins/variables.py b/plugins/variables.py
deleted file mode 100644
index 5c4bb79090..0000000000
--- a/plugins/variables.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# 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
-# .
-"""
-✘ Commands Available -
-
-• `{i}get var `
- Get value of the given variable name.
-
-• `{i}get type `
- Get variable type.
-
-• `{i}get db `
- Get db value of the given key.
-
-• `{i}get keys`
- Get all redis keys.
-"""
-
-import os
-
-from . import eor, get_string, udB, ultroid_cmd, HNDLR
-
-
-@ultroid_cmd(pattern="get($| (.*))", fullsudo=True)
-async def get_var(event):
- try:
- opt = event.text.split(maxsplit=2)[1]
- except IndexError:
- return await event.eor(f"what to get?\nRead `{HNDLR}help variables`")
- x = await event.eor(get_string("com_1"))
- if opt != "keys":
- try:
- varname = event.text.split(maxsplit=2)[2]
- except IndexError:
- return await eor(x, "Such a var doesn't exist!", time=5)
- if opt == "var":
- c = 0
- # try redis
- val = udB.get_key(varname)
- if val is not None:
- c += 1
- await x.edit(
- f"**Variable** - `{varname}`\n**Value**: `{val}`\n**Type**: Redis Key."
- )
- # try env vars
- val = os.getenv(varname)
- if val is not None:
- c += 1
- await x.edit(
- f"**Variable** - `{varname}`\n**Value**: `{val}`\n**Type**: Env Var."
- )
-
- if c == 0:
- await eor(x, "Such a var doesn't exist!", time=5)
-
- elif opt == "type":
- c = 0
- # try redis
- val = udB.get_key(varname)
- if val is not None:
- c += 1
- await x.edit(f"**Variable** - `{varname}`\n**Type**: Redis Key.")
- # try env vars
- val = os.getenv(varname)
- if val is not None:
- c += 1
- await x.edit(f"**Variable** - `{varname}`\n**Type**: Env Var.")
-
- if c == 0:
- await eor(x, "Such a var doesn't exist!", time=5)
-
- elif opt == "db":
- val = udB.get(varname)
- if val is not None:
- await x.edit(f"**Key** - `{varname}`\n**Value**: `{val}`")
- else:
- await eor(x, "No such key!", time=5)
-
- elif opt == "keys":
- keys = sorted(udB.keys())
- msg = "".join(
- f"• `{i}`" + "\n"
- for i in keys
- if not i.isdigit()
- and not i.startswith("-")
- and not i.startswith("_")
- and not i.startswith("GBAN_REASON_")
- )
-
- await x.edit(f"**List of DB Keys :**\n{msg}")
diff --git a/plugins/videotools.py b/plugins/videotools.py
deleted file mode 100644
index b25081bd88..0000000000
--- a/plugins/videotools.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# 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
-# .
-"""
-✘ Commands Available -
-
-•`{i}sample `
- Creates Short sample of video..
-
-• `{i}vshots `
- Creates screenshot of video..
-
-• `{i}vtrim - in seconds`
- Crop a Lengthy video..
-"""
-
-import glob
-import os
-
-from pyUltroid.fns.tools import set_attributes
-
-from . import (
- ULTConfig,
- bash,
- duration_s,
- eod,
- genss,
- get_string,
- mediainfo,
- stdr,
- ultroid_cmd,
-)
-
-
-@ultroid_cmd(pattern="sample( (.*)|$)")
-async def gen_sample(e):
- sec = e.pattern_match.group(1).strip()
- stime = int(sec) if sec and sec.isdigit() else 30
- vido = await e.get_reply_message()
- if vido and vido.media and "video" in mediainfo(vido.media):
- msg = await e.eor(get_string("com_1"))
- file, _ = await e.client.fast_downloader(
- vido.document, show_progress=True, event=msg
- )
- file_name = (file.name).split("/")[-1]
- out = file_name.replace(file_name.split(".")[-1], "_sample.mkv")
- xxx = await msg.edit(f"Generating Sample of `{stime}` seconds...")
- ss, dd = await duration_s(file.name, stime)
- cmd = f'ffmpeg -i "{file.name}" -preset ultrafast -ss {ss} -to {dd} -codec copy -map 0 "{out}" -y'
- await bash(cmd)
- os.remove(file.name)
- attributes = await set_attributes(out)
- mmmm, _ = await e.client.fast_uploader(
- out, show_progress=True, event=xxx, to_delete=True
- )
- caption = f"A Sample Video Of `{stime}` seconds"
- await e.client.send_file(
- e.chat_id,
- mmmm,
- thumb=ULTConfig.thumb,
- caption=caption,
- attributes=attributes,
- force_document=False,
- reply_to=e.reply_to_msg_id,
- )
- await xxx.delete()
- else:
- await e.eor(get_string("audiotools_8"), time=5)
-
-
-@ultroid_cmd(pattern="vshots( (.*)|$)")
-async def gen_shots(e):
- ss = e.pattern_match.group(1).strip()
- shot = int(ss) if ss and ss.isdigit() else 5
- vido = await e.get_reply_message()
- if vido and vido.media and "video" in mediainfo(vido.media):
- msg = await e.eor(get_string("com_1"))
- file, _ = await e.client.fast_downloader(
- vido.document, show_progress=True, event=msg
- )
- xxx = await msg.edit(f"Generating `{shot}` screenshots...")
- await bash("rm -rf ss && mkdir ss")
- cmd = f'ffmpeg -i "{file.name}" -vf fps=0.009 -vframes {shot} "ss/pic%01d.png"'
- await bash(cmd)
- os.remove(file.name)
- pic = glob.glob("ss/*")
- text = f"Uploaded {len(pic)}/{shot} screenshots"
- if not pic:
- text = "`Failed to Take Screenshots..`"
- pic = None
- await e.respond(text, file=pic)
- await bash("rm -rf ss")
- await xxx.delete()
-
-
-@ultroid_cmd(pattern="vtrim( (.*)|$)")
-async def gen_sample(e):
- sec = e.pattern_match.group(1).strip()
- if not sec or "-" not in sec:
- return await eod(e, get_string("audiotools_3"))
- a, b = sec.split("-")
- if int(a) >= int(b):
- return await eod(e, get_string("audiotools_4"))
- vido = await e.get_reply_message()
- if vido and vido.media and "video" in mediainfo(vido.media):
- msg = await e.eor(get_string("audiotools_5"))
- file, _ = await e.client.fast_downloader(
- vido.document, show_progress=True, event=msg
- )
- file_name = (file.name).split("/")[-1]
- out = file_name.replace(file_name.split(".")[-1], "_trimmed.mkv")
- if int(b) > int(await genss(file.name)):
- os.remove(file.name)
- return await eod(msg, get_string("audiotools_6"))
- ss, dd = stdr(int(a)), stdr(int(b))
- xxx = await msg.edit(f"Trimming Video from `{ss}` to `{dd}`...")
- cmd = f'ffmpeg -i "{file.name}" -preset ultrafast -ss {ss} -to {dd} -codec copy -map 0 "{out}" -y'
- await bash(cmd)
- os.remove(file.name)
- attributes = await set_attributes(out)
- mmmm, _ = await e.client.fast_uploader(
- out, show_progress=True, event=msg, to_delete=True
- )
- caption = f"Trimmed Video From `{ss}` To `{dd}`"
- await e.client.send_file(
- e.chat_id,
- mmmm,
- thumb=ULTConfig.thumb,
- caption=caption,
- attributes=attributes,
- force_document=False,
- reply_to=e.reply_to_msg_id,
- )
- await xxx.delete()
- else:
- await e.eor(get_string("audiotools_8"), time=5)
diff --git a/plugins/warn.py b/plugins/warn.py
deleted file mode 100644
index 8d3a52562c..0000000000
--- a/plugins/warn.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# 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
-# .
-"""
-✘ Commands Available
-
-•`{i}warn `
- Gives Warn.
-
-•`{i}resetwarn `
- To reset All Warns.
-
-•`{i}warns `
- To Get List of Warnings of a user.
-
-•`{i}setwarn | `
- Set Number in warn count for warnings
- After putting " | " mark put action like ban/mute/kick
- Its Default 3 kick
- Example : `setwarn 5 | mute`
-
-"""
-
-from pyUltroid.dB.warn_db import add_warn, reset_warn, warns
-
-from . import eor, get_string, inline_mention, udB, ultroid_cmd
-
-
-@ultroid_cmd(
- pattern="warn( (.*)|$)",
- manager=True,
- groups_only=True,
- admins_only=True,
-)
-async def warn(e):
- ultroid_bot = e.client
- reply = await e.get_reply_message()
- if len(e.text) > 5 and " " not in e.text[5]:
- return
- if reply:
- user = reply.sender_id
- reason = e.text[5:] if e.pattern_match.group(1).strip() else "unknown"
- else:
- try:
- user = e.text.split()[1]
- if user.startswith("@"):
- ok = await ultroid_bot.get_entity(user)
- user = ok.id
- else:
- user = int(user)
- except BaseException:
- return await e.eor("Reply To A User", time=5)
- try:
- reason = e.text.split(maxsplit=2)[-1]
- except BaseException:
- reason = "unknown"
- count, r = warns(e.chat_id, user)
- r = f"{r}|$|{reason}" if r else reason
- try:
- x = udB.get_key("SETWARN")
- number, action = int(x.split()[0]), x.split()[1]
- except BaseException:
- number, action = 3, "kick"
- if ("ban" or "kick" or "mute") not in action:
- action = "kick"
- if count + 1 >= number:
- if "ban" in action:
- try:
- await ultroid_bot.edit_permissions(e.chat_id, user, view_messages=False)
- except BaseException:
- return await e.eor("`Something Went Wrong.`", time=5)
- elif "kick" in action:
- try:
- await ultroid_bot.kick_participant(e.chat_id, user)
- except BaseException:
- return await e.eor("`Something Went Wrong.`", time=5)
- elif "mute" in action:
- try:
- await ultroid_bot.edit_permissions(
- e.chat_id, user, until_date=None, send_messages=False
- )
- except BaseException:
- return await e.eor("`Something Went Wrong.`", time=5)
- add_warn(e.chat_id, user, count + 1, r)
- c, r = warns(e.chat_id, user)
- ok = await ultroid_bot.get_entity(user)
- user = inline_mention(ok)
- r = r.split("|$|")
- text = f"User {user} Got {action} Due to {count+1} Warns.\n\n"
- for x in range(c):
- text += f"•**{x+1}.** {r[x]}\n"
- await e.eor(text)
- return reset_warn(e.chat_id, ok.id)
- add_warn(e.chat_id, user, count + 1, r)
- ok = await ultroid_bot.get_entity(user)
- user = inline_mention(ok)
- await eor(
- e,
- f"**WARNING :** {count+1}/{number}\n**To :**{user}\n**Be Careful !!!**\n\n**Reason** : {reason}",
- )
-
-
-@ultroid_cmd(
- pattern="resetwarn( (.*)|$)",
- manager=True,
- groups_only=True,
- admins_only=True,
-)
-async def rwarn(e):
- reply = await e.get_reply_message()
- if reply:
- user = reply.sender_id
- else:
- try:
- user = e.text.split()[1]
- if user.startswith("@"):
- ok = await e.client.get_entity(user)
- user = ok.id
- else:
- user = int(user)
- except BaseException:
- return await e.eor("Reply To user")
- reset_warn(e.chat_id, user)
- ok = await e.client.get_entity(user)
- user = inline_mention(ok)
- await e.eor(f"Cleared All Warns of {user}.")
-
-
-@ultroid_cmd(
- pattern="warns( (.*)|$)",
- manager=True,
- groups_only=True,
- admins_only=True,
-)
-async def twarns(e):
- reply = await e.get_reply_message()
- if reply:
- user = reply.from_id.user_id
- else:
- try:
- user = e.text.split()[1]
- if user.startswith("@"):
- ok = await e.client.get_entity(user)
- user = ok.id
- else:
- user = int(user)
- except BaseException:
- return await e.eor("Reply To A User", time=5)
- c, r = warns(e.chat_id, user)
- if c and r:
- ok = await e.client.get_entity(user)
- user = inline_mention(ok)
- r = r.split("|$|")
- text = f"User {user} Got {c} Warns.\n\n"
- for x in range(c):
- text += f"•**{x+1}.** {r[x]}\n"
- await e.eor(text)
- else:
- await e.eor("`No Warnings`")
-
-
-@ultroid_cmd(pattern="setwarn( (.*)|$)", manager=True)
-async def warnset(e):
- ok = e.pattern_match.group(1).strip()
- if not ok:
- return await e.eor("Invalid format. Correct usage: .setwarns |")
- if "|" in ok:
- try:
- number, action = ok.split("|")
- number = int(number.strip())
- action = action.strip()
- except ValueError:
- return await e.eor(
- "Invalid format. Correct usage: .setwarns |", time=5
- )
- if action not in ["ban", "mute", "kick"]:
- return await e.eor("Only mute / ban / kick options are supported", time=5)
- udB.set_key("SETWARN", f"{number} {action}")
- await e.eor(f"Done. Your Warn Count is now {number} and Action is {action}")
- else:
- await e.eor(
- "Invalid format. Correct usage: .setwarns |", time=5
- )
diff --git a/plugins/weather.py b/plugins/weather.py
deleted file mode 100644
index 26420330ec..0000000000
--- a/plugins/weather.py
+++ /dev/null
@@ -1,176 +0,0 @@
-# Ultroid ~ UserBot
-# Copyright (C) 2023-2024 TeamUltroid
-#
-# This file is a part of < https://github.com/TeamUltroid/Ultroid/ >
-# PLease read the GNU Affero General Public License in
-# .
-
-"""
-**Get Weather Data using OpenWeatherMap API**
-❍ Commands Available -
-
-• `{i}weather`
- Shows the Weather of Cities
-
-• `{i}air`
- Shows the Air Condition of Cities
-"""
-
-import datetime
-import time
-from datetime import timedelta
-
-import aiohttp
-import pytz
-
-from . import async_searcher, get_string, udB, ultroid_cmd
-
-
-async def get_timezone(offset_seconds, use_utc=False):
- offset = timedelta(seconds=offset_seconds)
- hours, remainder = divmod(offset.seconds, 3600)
- sign = "+" if offset.total_seconds() >= 0 else "-"
- timezone = "UTC" if use_utc else "GMT"
- if use_utc:
- for m in pytz.all_timezones:
- tz = pytz.timezone(m)
- now = datetime.datetime.now(tz)
- if now.utcoffset() == offset:
- return f"{m} ({timezone}{sign}{hours:02d})"
- else:
- for m in pytz.all_timezones:
- tz = pytz.timezone(m)
- if m.startswith("Australia/"):
- now = datetime.datetime.now(tz)
- if now.utcoffset() == offset:
- return f"{m} ({timezone}{sign}{hours:02d})"
- for m in pytz.all_timezones:
- tz = pytz.timezone(m)
- now = datetime.datetime.now(tz)
- if now.utcoffset() == offset:
- return f"{m} ({timezone}{sign}{hours:02d})"
- return "Timezone not found"
-
-async def getWindinfo(speed: str, degree: str) -> str:
- dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
- ix = round(degree / (360.00 / len(dirs)))
- kmph = str(float(speed) * 3.6) + " km/h"
- return f"[{dirs[ix % len(dirs)]}] {kmph}"
-
-async def get_air_pollution_data(latitude, longitude, api_key):
- url = f"http://api.openweathermap.org/data/2.5/air_pollution?lat={latitude}&lon={longitude}&appid={api_key}"
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as response:
- data = await response.json()
- if "list" in data:
- air_pollution = data["list"][0]
- return air_pollution
- else:
- return None
-
-
-@ultroid_cmd(pattern="weather ?(.*)")
-async def weather(event):
- if event.fwd_from:
- return
- msg = await event.eor(get_string("com_1"))
- x = udB.get_key("OPENWEATHER_API")
- if x is None:
- await event.eor(
- "No API found. Get One from [Here](https://api.openweathermap.org)\nAnd Add it in OPENWEATHER_API Redis Key",
- time=8,
- )
- return
- input_str = event.pattern_match.group(1)
- if not input_str:
- await event.eor("No Location was Given...", time=5)
- return
- elif input_str == "butler":
- await event.eor("search butler,au for australila", time=5)
- sample_url = f"https://api.openweathermap.org/data/2.5/weather?q={input_str}&APPID={x}&units=metric"
- try:
- response_api = await async_searcher(sample_url, re_json=True)
- if response_api["cod"] == 200:
- country_time_zone = int(response_api["timezone"])
- tz = f"{await get_timezone(country_time_zone)}"
- sun_rise_time = int(response_api["sys"]["sunrise"]) + country_time_zone
- sun_set_time = int(response_api["sys"]["sunset"]) + country_time_zone
- await msg.edit(
- f"{response_api['name']}, {response_api['sys']['country']}\n\n"
- f"╭────────────────•\n"
- f"╰➢ **𝖶𝖾𝖺𝗍𝗁𝖾𝗋:** {response_api['weather'][0]['description']}\n"
- f"╰➢ **𝖳𝗂𝗆𝖾𝗓𝗈𝗇𝖾:** {tz}\n"
- f"╰➢ **𝖲𝗎𝗇𝗋𝗂𝗌𝖾:** {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(sun_rise_time))}\n"
- f"╰➢ **𝖲𝗎𝗇𝗌𝖾𝗍:** {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(sun_set_time))}\n"
- f"╰➢ **𝖶𝗂𝗇𝖽:** {await getWindinfo(response_api['wind']['speed'], response_api['wind']['deg'])}\n"
- f"╰➢ **𝖳𝖾𝗆𝗉𝖾𝗋𝖺𝗍𝗎𝗋𝖾:** {response_api['main']['temp']}°C\n"
- f"╰➢ **𝖥𝖾𝖾𝗅𝗌 𝗅𝗂𝗄𝖾:** {response_api['main']['feels_like']}°C\n"
- f"╰➢ **𝖬𝗂𝗇𝗂𝗆𝗎𝗆:** {response_api['main']['temp_min']}°C\n"
- f"╰➢ **𝖬𝖺𝗑𝗂𝗆𝗎𝗆:** {response_api['main']['temp_max']}°C\n"
- f"╰➢ **𝖯𝗋𝖾𝗌𝗌𝗎𝗋𝖾:** {response_api['main']['pressure']} hPa\n"
- f"╰➢ **𝖧𝗎𝗆𝗂𝖽𝗂𝗍𝗒:** {response_api['main']['humidity']}%\n"
- f"╰➢ **𝖵𝗂𝗌𝗂𝖻𝗂𝗅𝗂𝗍𝗒:** {response_api['visibility']} m\n"
- f"╰➢ **𝖢𝗅𝗈𝗎𝖽𝗌:** {response_api['clouds']['all']}%\n"
- f"╰────────────────•\n\n"
- )
- else:
- await msg.edit(response_api["message"])
- except Exception as e:
- await event.eor(f"An unexpected error occurred: {str(e)}", time=5)
-
-
-@ultroid_cmd(pattern="air ?(.*)")
-async def air_pollution(event):
- if event.fwd_from:
- return
- msg = await event.eor(get_string("com_1"))
- x = udB.get_key("OPENWEATHER_API")
- if x is None:
- await event.eor(
- "No API found. Get One from [Here](https://api.openweathermap.org)\nAnd Add it in OPENWEATHER_API Redis Key",
- time=8,
- )
- return
- input_str = event.pattern_match.group(1)
- if not input_str:
- await event.eor("`No Location was Given...`", time=5)
- return
- if input_str.lower() == "perth":
- geo_url = f"https://geocode.xyz/perth%20au?json=1"
- else:
- geo_url = f"https://geocode.xyz/{input_str}?json=1"
- geo_data = await async_searcher(geo_url, re_json=True)
- try:
- longitude = geo_data["longt"]
- latitude = geo_data["latt"]
- except KeyError as e:
- LOGS.info(e)
- await event.eor("`Unable to find coordinates for the given location.`", time=5)
- return
- try:
- city = geo_data["standard"]["city"]
- prov = geo_data["standard"]["prov"]
- except KeyError as e:
- LOGS.info(e)
- await event.eor("`Unable to find city for the given coordinates.`", time=5)
- return
- air_pollution_data = await get_air_pollution_data(latitude, longitude, x)
- if air_pollution_data is None:
- await event.eor(
- "`Unable to fetch air pollution data for the given location.`", time=5
- )
- return
- await msg.edit(
- f"{city}, {prov}\n\n"
- f"╭────────────────•\n"
- f"╰➢ **𝖠𝖰𝖨:** {air_pollution_data['main']['aqi']}\n"
- f"╰➢ **𝖢𝖺𝗋𝖻𝗈𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['co']}µg/m³\n"
- f"╰➢ **𝖭𝗈𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖬𝗈𝗇𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no']}µg/m³\n"
- f"╰➢ **𝖭𝗂𝗍𝗋𝗈𝗀𝖾𝗇 𝖣𝗂𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['no2']}µg/m³\n"
- f"╰➢ **𝖮𝗓𝗈𝗇𝖾:** {air_pollution_data['components']['o3']}µg/m³\n"
- f"╰➢ **𝖲𝗎𝗅𝗉𝗁𝗎𝗋 𝖣𝗂𝗈𝗑𝗂𝖽𝖾:** {air_pollution_data['components']['so2']}µg/m³\n"
- f"╰➢ **𝖠𝗆𝗆𝗈𝗇𝗂𝖺:** {air_pollution_data['components']['nh3']}µg/m³\n"
- f"╰➢ **𝖥𝗂𝗇𝖾 𝖯𝖺𝗋𝗍𝗂𝖼𝗅𝖾𝗌 (PM₂.₅):** {air_pollution_data['components']['pm2_5']}\n"
- f"╰➢ **𝖢𝗈𝖺𝗋𝗌𝖾 𝖯𝖺𝗋𝗍𝗂𝖼𝗅𝖾𝗌 (PM₁₀):** {air_pollution_data['components']['pm10']}\n"
- f"╰────────────────•\n\n"
- )
diff --git a/plugins/webupload.py b/plugins/webupload.py
deleted file mode 100644
index d319fa277a..0000000000
--- a/plugins/webupload.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# 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
-# .
-
-"""
-✘ Commands Available -
-
-• `{i}webupload`
- Upload files on another server.
-"""
-
-import os
-
-from pyUltroid.fns.tools import _webupload_cache
-
-from . import Button, asst, get_string, ultroid_cmd
-
-
-@ultroid_cmd(pattern="webupload( (.*)|$)")
-async def _(event):
- xx = await event.eor(get_string("com_1"))
- match = event.pattern_match.group(1).strip()
- if event.chat_id not in _webupload_cache:
- _webupload_cache.update({int(event.chat_id): {}})
- if match:
- if not os.path.exists(match):
- return await xx.eor("File doesn't exist.")
- _webupload_cache[event.chat_id][event.id] = match
- elif event.reply_to_msg_id:
- reply = await event.get_reply_message()
- if reply.photo:
- file = await reply.download_media("resources/downloads/")
- _webupload_cache[int(event.chat_id)][int(event.id)] = file
- else:
- file, _ = await event.client.fast_downloader(
- reply.document, show_progress=True, event=xx
- )
- _webupload_cache[int(event.chat_id)][int(event.id)] = file.name
- else:
- return await xx.eor("Reply to file or give file path...")
- if not event.client._bot:
- results = await event.client.inline_query(
- asst.me.username, f"fl2lnk {event.chat_id}:{event.id}"
- )
- await results[0].click(event.chat_id, reply_to=event.reply_to_msg_id)
- await xx.delete()
-
- else:
- __cache = f"{event.chat_id}:{event.id}"
- buttons = [
- [
- Button.inline("catbox", data=f"flcatbox//{__cache}"),
- Button.inline("transfer", data=f"fltransfer//{__cache}"),
- ],
- [
- Button.inline("filebin", data=f"flfilebin//{__cache}"),
- Button.inline("0x0.st", data=f"fl0x0.st//{__cache}"),
- ],
- [
- Button.inline("file.io", data=f"flfile.io//{__cache}"),
- Button.inline("siasky", data=f"flsiasky//{__cache}"),
- ],
- ]
- await xx.edit("Choose Server to Upload File...", buttons=buttons)
diff --git a/plugins/words.py b/plugins/words.py
deleted file mode 100644
index 22e5f8d73f..0000000000
--- a/plugins/words.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# 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
-# .
-"""
-✘ Commands Available -
-
-• `{i}meaning `
- Get the meaning of the word.
-
-• `{i}synonym `
- Get all synonyms.
-
-• `{i}antonym `
- Get all antonyms.
-
-• `{i}ud `
- Fetch word defenition from urbandictionary.
-"""
-import io
-
-from pyUltroid.fns.misc import get_synonyms_or_antonyms
-from pyUltroid.fns.tools import async_searcher
-
-from . import get_string, ultroid_cmd
-
-
-@ultroid_cmd(pattern="meaning( (.*)|$)", manager=True)
-async def mean(event):
- wrd = event.pattern_match.group(1).strip()
- if not wrd:
- return await event.eor(get_string("wrd_4"))
- url = f"https://api.dictionaryapi.dev/api/v2/entries/en/{wrd}"
- out = await async_searcher(url, re_json=True)
- try:
- return await event.eor(f'**{out["title"]}**')
- except (KeyError, TypeError):
- pass
- defi = out[0]["meanings"][0]["definitions"][0]
- ex = defi["example"] if defi.get("example") else "None"
- text = get_string("wrd_1").format(wrd, defi["definition"], ex)
- if defi.get("synonyms"):
- text += (
- f"\n\n• **{get_string('wrd_5')} :**"
- + "".join(f" {a}," for a in defi["synonyms"])[:-1][:10]
- )
- if defi.get("antonyms"):
- text += (
- f"\n\n**{get_string('wrd_6')} :**"
- + "".join(f" {a}," for a in defi["antonyms"])[:-1][:10]
- )
- if len(text) > 4096:
- with io.BytesIO(str.encode(text)) as fle:
- fle.name = f"{wrd}-meanings.txt"
- await event.reply(
- file=fle,
- force_document=True,
- caption=f"Meanings of {wrd}",
- )
- await event.delete()
- else:
- await event.eor(text)
-
-
-@ultroid_cmd(
- pattern="(syno|anto)nym",
-)
-async def mean(event):
- task = event.pattern_match.group(1) + "nyms"
- try:
- wrd = event.text.split(maxsplit=1)[1]
- except IndexError:
- return await event.eor("Give Something to search..")
- try:
- ok = await get_synonyms_or_antonyms(wrd, task)
- x = get_string("wrd_2" if task == "synonyms" else "wrd_3").format(wrd)
- for c, i in enumerate(ok, start=1):
- x += f"**{c}.** `{i}`\n"
- if len(x) > 4096:
- with io.BytesIO(str.encode(x)) as fle:
- fle.name = f"{wrd}-{task}.txt"
- await event.client.send_file(
- event.chat_id,
- fle,
- force_document=True,
- allow_cache=False,
- caption=f"{task} of {wrd}",
- reply_to=event.reply_to_msg_id,
- )
- await event.delete()
- else:
- await event.eor(x)
- except Exception as e:
- await event.eor(
- get_string("wrd_7" if task == "synonyms" else "wrd_8").format(e)
- )
-
-
-@ultroid_cmd(pattern="ud (.*)")
-async def _(event):
- word = event.pattern_match.group(1).strip()
- if not word:
- return await event.eor(get_string("autopic_1"))
- out = await async_searcher(
- "http://api.urbandictionary.com/v0/define", params={"term": word}, re_json=True
- )
- try:
- out = out["list"][0]
- except IndexError:
- return await event.eor(get_string("autopic_2").format(word))
- await event.eor(
- get_string("wrd_1").format(out["word"], out["definition"], out["example"]),
- )
diff --git a/plugins/writer.py b/plugins/writer.py
deleted file mode 100644
index 9b9ea3d4e7..0000000000
--- a/plugins/writer.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# 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
-# .
-
-"""
-✘ Commands Available -
-
-• `{i}write `
- It will write on a paper.
-
-• `{i}image `
- Write a image from html or any text.
-"""
-
-import os
-
-from htmlwebshot import WebShot
-from PIL import Image, ImageDraw, ImageFont
-
-from . import async_searcher, eod, get_string, text_set, ultroid_cmd
-
-
-@ultroid_cmd(pattern="gethtml( (.*)|$)")
-async def ghtml(e):
- if txt := e.pattern_match.group(1).strip():
- link = e.text.split(maxsplit=1)[1]
- else:
- return await eod(e, "`Either reply to any file or give any text`")
- k = await async_searcher(link)
- with open("file.html", "w+") as f:
- f.write(k)
- await e.reply(file="file.html")
-
-
-@ultroid_cmd(pattern="image( (.*)|$)")
-async def f2i(e):
- txt = e.pattern_match.group(1).strip()
- html = None
- if txt:
- html = e.text.split(maxsplit=1)[1]
- elif e.reply_to:
- r = await e.get_reply_message()
- if r.media:
- html = await e.client.download_media(r.media)
- elif r.text:
- html = r.text
- if not html:
- return await eod(e, "`Either reply to any file or give any text`")
- html = html.replace("\n", "
")
- shot = WebShot(quality=85)
- css = "body {background: white;} p {color: red;}"
- pic = await shot.create_pic_async(html=html, css=css)
- await e.reply(file=pic, force_document=True)
- os.remove(pic)
- if os.path.exists(html):
- os.remove(html)
-
-
-@ultroid_cmd(pattern="write( (.*)|$)")
-async def writer(e):
- if e.reply_to:
- reply = await e.get_reply_message()
- text = reply.message
- elif e.pattern_match.group(1).strip():
- text = e.text.split(maxsplit=1)[1]
- else:
- return await eod(e, get_string("writer_1"))
- k = await e.eor(get_string("com_1"))
- img = Image.open("resources/extras/template.jpg")
- draw = ImageDraw.Draw(img)
- font = ImageFont.truetype("resources/fonts/assfont.ttf", 30)
- x, y = 150, 140
- lines = text_set(text)
- bbox = font.getbbox("hg")
- line_height = bbox[3] - bbox[1]
- for line in lines:
- draw.text((x, y), line, fill=(1, 22, 55), font=font)
- y = y + line_height - 5
- file = "ult.jpg"
- img.save(file)
- await e.reply(file=file)
- os.remove(file)
- await k.delete()
diff --git a/plugins/youtube.py b/plugins/youtube.py
deleted file mode 100644
index fc129c0d48..0000000000
--- a/plugins/youtube.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# 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
-# .
-"""
-✘ Commands Available -
-
-• `{i}yta <(youtube/any) link>`
- Download audio from the link.
-
-• `{i}ytv <(youtube/any) link>`
- Download video from the link.
-
-• `{i}ytsa <(youtube) search query>`
- Search and download audio from youtube.
-
-• `{i}ytsv <(youtube) search query>`
- Search and download video from youtube.
-"""
-from pyUltroid.fns.ytdl import download_yt, get_yt_link
-
-from . import get_string, requests, ultroid_cmd
-
-
-@ultroid_cmd(
- pattern="yt(a|v|sa|sv) ?(.*)",
-)
-async def download_from_youtube_(event):
- ytd = {
- "prefer_ffmpeg": True,
- "addmetadata": True,
- "geo-bypass": True,
- "nocheckcertificate": True,
- }
- opt = event.pattern_match.group(1).strip()
- xx = await event.eor(get_string("com_1"))
- if opt == "a":
- ytd["format"] = "bestaudio"
- ytd["outtmpl"] = "%(id)s.m4a"
- url = event.pattern_match.group(2)
- if not url:
- return await xx.eor(get_string("youtube_1"))
- try:
- requests.get(url)
- except BaseException:
- return await xx.eor(get_string("youtube_2"))
- elif opt == "v":
- ytd["format"] = "best"
- ytd["outtmpl"] = "%(id)s.mp4"
- ytd["postprocessors"] = [{"key": "FFmpegMetadata"}]
- url = event.pattern_match.group(2)
- if not url:
- return await xx.eor(get_string("youtube_3"))
- try:
- requests.get(url)
- except BaseException:
- return await xx.eor(get_string("youtube_4"))
- elif opt == "sa":
- ytd["format"] = "bestaudio"
- ytd["outtmpl"] = "%(id)s.m4a"
- try:
- query = event.text.split(" ", 1)[1]
- except IndexError:
- return await xx.eor(get_string("youtube_5"))
- url = get_yt_link(query)
- if not url:
- return await xx.edit(get_string("unspl_1"))
- await xx.eor(get_string("youtube_6"))
- elif opt == "sv":
- ytd["format"] = "best"
- ytd["outtmpl"] = "%(id)s.mp4"
- ytd["postprocessors"] = [{"key": "FFmpegMetadata"}]
- try:
- query = event.text.split(" ", 1)[1]
- except IndexError:
- return await xx.eor(get_string("youtube_7"))
- url = get_yt_link(query)
- if not url:
- return await xx.edit(get_string("unspl_1"))
- await xx.eor(get_string("youtube_8"))
- else:
- return
- await download_yt(xx, url, ytd)
diff --git a/plugins/ziptools.py b/plugins/ziptools.py
deleted file mode 100644
index ab2674c191..0000000000
--- a/plugins/ziptools.py
+++ /dev/null
@@ -1,172 +0,0 @@
-# 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
-# .
-"""
-✘ Commands Available
-
-• `{i}zip `
- zip the replied file
- To set password on zip: `{i}zip ` reply to file
-
-• `{i}unzip `
- unzip the replied file.
-
-• `{i}azip `
- add file to batch for batch upload zip
-
-• `{i}dozip`
- upload batch zip the files u added from `{i}azip`
- To set Password: `{i}dozip `
-
-"""
-import os
-import time
-
-from . import (
- HNDLR,
- ULTConfig,
- asyncio,
- bash,
- downloader,
- get_all_files,
- get_string,
- ultroid_cmd,
- uploader,
-)
-
-
-@ultroid_cmd(pattern="zip( (.*)|$)")
-async def zipp(event):
- reply = await event.get_reply_message()
- t = time.time()
- if not reply:
- await event.eor(get_string("zip_1"))
- return
- xx = await event.eor(get_string("com_1"))
- if reply.media:
- if hasattr(reply.media, "document"):
- file = reply.media.document
- image = await downloader(
- reply.file.name, reply.media.document, xx, t, get_string("com_5")
- )
- file = image.name
- else:
- file = await event.download_media(reply)
- inp = file.replace(file.split(".")[-1], "zip")
- if event.pattern_match.group(1).strip():
- await bash(
- f"zip -r --password {event.pattern_match.group(1).strip()} {inp} {file}"
- )
- else:
- await bash(f"zip -r {inp} {file}")
- k = time.time()
- n_file, _ = await event.client.fast_uploader(
- inp, show_progress=True, event=event, message="Uploading...", to_delete=True
- )
- await event.client.send_file(
- event.chat_id,
- n_file,
- force_document=True,
- thumb=ULTConfig.thumb,
- caption=f"`{n_file.name}`",
- reply_to=reply,
- )
- os.remove(inp)
- os.remove(file)
- await xx.delete()
-
-
-@ultroid_cmd(pattern="unzip( (.*)|$)")
-async def unzipp(event):
- reply = await event.get_reply_message()
- file = event.pattern_match.group(1).strip()
- t = time.time()
- if not ((reply and reply.media) or file):
- await event.eor(get_string("zip_1"))
- return
- xx = await event.eor(get_string("com_1"))
- if reply.media:
- if not hasattr(reply.media, "document"):
- return await xx.edit(get_string("zip_3"))
- file = reply.media.document
- if not reply.file.name.endswith(("zip", "rar", "exe")):
- return await xx.edit(get_string("zip_3"))
- image = await downloader(
- reply.file.name, reply.media.document, xx, t, get_string("com_5")
- )
- file = image.name
- if os.path.isdir("unzip"):
- await bash("rm -rf unzip")
- os.mkdir("unzip")
- await bash(f"7z x {file} -aoa -ounzip")
- await asyncio.sleep(4)
- ok = get_all_files("unzip")
- for x in ok:
- k = time.time()
- n_file, _ = await event.client.fast_uploader(
- x, show_progress=True, event=event, message="Uploading...", to_delete=True
- )
- await event.client.send_file(
- event.chat_id,
- n_file,
- force_document=True,
- thumb=ULTConfig.thumb,
- caption=f"`{n_file.name}`",
- )
- await xx.delete()
-
-
-@ultroid_cmd(pattern="addzip$")
-async def azipp(event):
- reply = await event.get_reply_message()
- t = time.time()
- if not (reply and reply.media):
- await event.eor(get_string("zip_1"))
- return
- xx = await event.eor(get_string("com_1"))
- if not os.path.isdir("zip"):
- os.mkdir("zip")
- if reply.media:
- if hasattr(reply.media, "document"):
- file = reply.media.document
- image = await downloader(
- f"zip/{reply.file.name}",
- reply.media.document,
- xx,
- t,
- get_string("com_5"),
- )
-
- file = image.name
- else:
- file = await event.download_media(reply.media, "zip/")
- await xx.edit(
- f"Downloaded `{file}` succesfully\nNow Reply To Other Files To Add And Zip all at once"
- )
-
-
-@ultroid_cmd(pattern="dozip( (.*)|$)")
-async def do_zip(event):
- if not os.path.isdir("zip"):
- return await event.eor(get_string("zip_2").format(HNDLR))
- xx = await event.eor(get_string("com_1"))
- if event.pattern_match.group(1).strip():
- await bash(
- f"zip -r --password {event.pattern_match.group(1).strip()} ultroid.zip zip/*"
- )
- else:
- await bash("zip -r ultroid.zip zip/*")
- k = time.time()
- xxx = await uploader("ultroid.zip", "ultroid.zip", k, xx, get_string("com_6"))
- await event.client.send_file(
- event.chat_id,
- xxx,
- force_document=True,
- thumb=ULTConfig.thumb,
- )
- await bash("rm -rf zip")
- os.remove("ultroid.zip")
- await xx.delete()
diff --git a/pyUltroid/__init__.py b/pyUltroid/__init__.py
index c20629b7f1..9ff6aa373f 100644
--- a/pyUltroid/__init__.py
+++ b/pyUltroid/__init__.py
@@ -6,8 +6,9 @@
# .
import os
-import sys
+import sys, asyncio
import telethonpatch
+from datetime import datetime
from .version import __version__
run_as_module = __package__ in sys.argv or sys.argv[0] == "-m"
@@ -36,10 +37,11 @@ class ULTConfig:
exit()
start_time = time.time()
+ _loop = asyncio.get_event_loop()
_ult_cache = {}
_ignore_eval = []
- udB = UltroidDB()
+ udB = _loop.run_until_complete(UltroidDB())
update_envs()
LOGS.info(f"Connecting to {udB.name}...")
@@ -99,7 +101,7 @@ class ULTConfig:
DUAL_HNDLR = udB.get_key("DUAL_HNDLR") or "/"
SUDO_HNDLR = udB.get_key("SUDO_HNDLR") or HNDLR
else:
- print("pyUltroid 2022 © TeamUltroid")
+ print(f"pyUltroid {datetime.now().year} © TeamUltroid")
from logging import getLogger
diff --git a/pyUltroid/__main__.py b/pyUltroid/__main__.py
index 9de436fbbd..cd1575e4e8 100644
--- a/pyUltroid/__main__.py
+++ b/pyUltroid/__main__.py
@@ -6,12 +6,17 @@
# .
from . import *
+from logging import getLogger
+from pyUltroid.web.server import ultroid_server
+
+logger = getLogger(__name__)
def main():
import os
import sys
import time
+ import asyncio
from .fns.helper import bash, time_formatter, updater
from .startup.funcs import (
@@ -21,14 +26,10 @@ def main():
plug,
ready,
startup_stuff,
+ user_sync_workflow,
)
from .startup.loader import load_other_plugins
- try:
- from apscheduler.schedulers.asyncio import AsyncIOScheduler
- except ImportError:
- AsyncIOScheduler = None
-
# Option to Auto Update On Restarts..
if (
udB.get_key("UPDATE_ON_RESTART")
@@ -48,22 +49,17 @@ def main():
LOGS.info("Initialising...")
- ultroid_bot.run_in_loop(autopilot())
+ # Execute critical startup functions immediately
+ ultroid_bot.run_in_loop(user_sync_workflow())
- pmbot = udB.get_key("PMBOT")
- manager = udB.get_key("MANAGER")
- addons = udB.get_key("ADDONS") or Var.ADDONS
- vcbot = udB.get_key("VCBOT") or Var.VCBOT
- if HOSTED_ON == "okteto":
- vcbot = False
-
- if (HOSTED_ON == "termux" or udB.get_key("LITE_DEPLOY")) and udB.get_key(
- "EXCLUDE_OFFICIAL"
- ) is None:
- _plugins = "autocorrect autopic audiotools compressor forcesubscribe fedutils gdrive glitch instagram nsfwfilter nightmode pdftools profanityfilter writer youtube"
- udB.set_key("EXCLUDE_OFFICIAL", _plugins)
-
- load_other_plugins(addons=addons, pmbot=pmbot, manager=manager, vcbot=vcbot)
+ # Load plugins first to ensure core functionality is available
+ # Store background tasks for later handling
+ background_tasks = load_other_plugins(
+ addons=udB.get_key("ADDONS") or Var.ADDONS or udB.get_key("INCLUDE_ALL"),
+ pmbot=udB.get_key("PMBOT"),
+ manager=udB.get_key("MANAGER"),
+ vcbot=udB.get_key("VCBOT") or Var.VCBOT,
+ )
suc_msg = """
----------------------------------------------------------------------
@@ -71,22 +67,22 @@ def main():
----------------------------------------------------------------------
"""
- # for channel plugins
+ # Schedule non-critical tasks as background tasks to improve startup time
plugin_channels = udB.get_key("PLUGIN_CHANNEL")
- # Customize Ultroid Assistant...
- ultroid_bot.run_in_loop(customize())
-
- # Load Addons from Plugin Channels.
+ # These operations are moved to background tasks to reduce startup time
+ asst.loop.create_task(autopilot())
+ if not USER_MODE and not udB.get_key("DISABLE_AST_PLUGINS"):
+ asst.loop.create_task(customize())
if plugin_channels:
- ultroid_bot.run_in_loop(plug(plugin_channels))
-
- # Send/Ignore Deploy Message..
+ asst.loop.create_task(plug(plugin_channels))
if not udB.get_key("LOG_OFF"):
- ultroid_bot.run_in_loop(ready())
+ asst.loop.create_task(ready())
+ asst.loop.create_task(WasItRestart(udB))
- # Edit Restarting Message (if It's restarting)
- ultroid_bot.run_in_loop(WasItRestart(udB))
+ if Var.START_WEB:
+ logger.info("Starting web server as a background task...")
+ asst.loop.create_task(ultroid_server.start())
try:
cleanup_cache()
@@ -99,7 +95,47 @@ def main():
LOGS.info(suc_msg)
+def run_indefinitely(max_wait: int = 3600 * 3): # 3 hours
+ """Run the assistant indefinitely with connection error handling and timeout.
+
+ Args:
+ max_wait: Maximum time in seconds to keep retrying on connection errors.
+ Defaults to 3 hours.
+ """
+ start_time = 0
+ retry_count = 0
+ backoff = 10 # Initial backoff time in seconds
+
+ while True:
+ # Check if max wait time exceeded
+ if start_time and (time.time() - start_time) > max_wait:
+ logger.error(f"Max wait time of {max_wait} seconds reached, exiting")
+ exit(1)
+
+ try:
+ # Attempt to run the assistant
+ asst.run()
+ except ConnectionError as er:
+ # Track first connection error
+ if not start_time:
+ start_time = time.time()
+
+ retry_count += 1
+ wait_time = min(backoff * retry_count, 300) # Cap at 5 minutes
+
+ logger.error(
+ f"ConnectionError: {er}, attempt {retry_count}, "
+ f"waiting {wait_time} seconds"
+ )
+ time.sleep(wait_time)
+ continue
+
+ except Exception as er:
+ logger.exception(f"Fatal error occurred: {er}")
+ exit(1)
+
+
if __name__ == "__main__":
main()
- asst.run()
+ run_indefinitely()
diff --git a/pyUltroid/_misc/_decorators.py b/pyUltroid/_misc/_decorators.py
index 94cea82976..b292f7c634 100644
--- a/pyUltroid/_misc/_decorators.py
+++ b/pyUltroid/_misc/_decorators.py
@@ -91,13 +91,13 @@ async def wrapp(ult):
if fullsudo and ult.sender_id not in SUDO_M.fullsudos:
return await eod(ult, get_string("py_d2"), time=15)
chat = ult.chat
- if hasattr(chat, "title"):
- if (
- "#noub" in chat.title.lower()
- and not (chat.admin_rights or chat.creator)
- and not (ult.sender_id in DEVLIST)
- ):
- return
+ # if hasattr(chat, "title"):
+ # if (
+ # "#noub" in chat.title.lower()
+ # and not (chat.admin_rights or chat.creator)
+ # and not (ult.sender_id in DEVLIST)
+ # ):
+ # return
if ult.is_private and (groups_only or admins_only):
return await eod(ult, get_string("py_d3"))
elif admins_only and not (chat.admin_rights or chat.creator):
diff --git a/pyUltroid/_misc/_wrappers.py b/pyUltroid/_misc/_wrappers.py
index 6625ec4d84..ed96baf117 100644
--- a/pyUltroid/_misc/_wrappers.py
+++ b/pyUltroid/_misc/_wrappers.py
@@ -52,7 +52,7 @@ async def eod(event, text=None, **kwargs):
async def _try_delete(event):
try:
return await event.delete()
- except (MessageDeleteForbiddenError):
+ except MessageDeleteForbiddenError:
pass
except BaseException as er:
from . import LOGS
diff --git a/pyUltroid/configs.py b/pyUltroid/configs.py
index ddf6f05a71..ecbb1cf895 100644
--- a/pyUltroid/configs.py
+++ b/pyUltroid/configs.py
@@ -53,3 +53,13 @@ class Var:
DATABASE_URL = config("DATABASE_URL", default=None)
# for MONGODB users
MONGO_URI = config("MONGO_URI", default=None)
+
+ START_WEB = config("START_WEB", default=False, cast=bool)
+ RENDER_WEB = config("RENDER_WEB", default=True, cast=bool)
+ PORT = config("PORT", default=8000, cast=int)
+ MINIAPP_URL = config("MINIAPP_URL", default=f"http://localhost:{PORT}")
+
+
+# CENTRAL_REPO_URL = "http://localhost:8055"
+CENTRAL_REPO_URL = "https://central.ultroid.org"
+ADMIN_BOT_USERNAME = "UltroidBot"
diff --git a/pyUltroid/dB/afk_db.py b/pyUltroid/dB/afk_db.py
deleted file mode 100644
index e6da109fd9..0000000000
--- a/pyUltroid/dB/afk_db.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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
-# .
-
-from datetime import datetime as dt
-
-from .. import udB
-
-
-def get_stuff():
- return udB.get_key("AFK_DB") or []
-
-
-def add_afk(msg, media_type, media):
- time = dt.now().strftime("%b %d %Y %I:%M:%S%p")
- udB.set_key("AFK_DB", [msg, media_type, media, time])
- return
-
-
-def is_afk():
- afk = get_stuff()
- if afk:
- start_time = dt.strptime(afk[3], "%b %d %Y %I:%M:%S%p")
- afk_since = str(dt.now().replace(microsecond=0) - start_time)
- return afk[0], afk[1], afk[2], afk_since
- return False
-
-
-def del_afk():
- return udB.del_key("AFK_DB")
diff --git a/pyUltroid/dB/antiflood_db.py b/pyUltroid/dB/antiflood_db.py
deleted file mode 100644
index 7c68d25f2a..0000000000
--- a/pyUltroid/dB/antiflood_db.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# 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
-# .
-
-
-from .. import udB
-
-
-def get_flood():
- return udB.get_key("ANTIFLOOD") or {}
-
-
-def set_flood(chat_id, limit):
- omk = get_flood()
- omk.update({chat_id: limit})
- return udB.set_key("ANTIFLOOD", omk)
-
-
-def get_flood_limit(chat_id):
- omk = get_flood()
- if chat_id in omk.keys():
- return omk[chat_id]
-
-
-def rem_flood(chat_id):
- omk = get_flood()
- if chat_id in omk.keys():
- del omk[chat_id]
- return udB.set_key("ANTIFLOOD", omk)
diff --git a/pyUltroid/dB/asstcmd_db.py b/pyUltroid/dB/asstcmd_db.py
deleted file mode 100644
index 9faa4f9cd2..0000000000
--- a/pyUltroid/dB/asstcmd_db.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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
-# .
-
-
-from .. import udB
-
-
-def get_stuff():
- return udB.get_key("ASST_CMDS") or {}
-
-
-def add_cmd(cmd, msg, media, button):
- ok = get_stuff()
- ok.update({cmd: {"msg": msg, "media": media, "button": button}})
- return udB.set_key("ASST_CMDS", ok)
-
-
-def rem_cmd(cmd):
- ok = get_stuff()
- if ok.get(cmd):
- ok.pop(cmd)
- return udB.set_key("ASST_CMDS", ok)
-
-
-def cmd_reply(cmd):
- ok = get_stuff()
- if ok.get(cmd):
- okk = ok[cmd]
- return okk["msg"], okk["media"], okk["button"] if ok.get("button") else None
- return
-
-
-def list_cmds():
- ok = get_stuff()
- return ok.keys()
diff --git a/pyUltroid/dB/blacklist_chat_db.py b/pyUltroid/dB/blacklist_chat_db.py
deleted file mode 100644
index e8bd874037..0000000000
--- a/pyUltroid/dB/blacklist_chat_db.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from .. import udB
-
-
-def add_black_chat(chat_id):
- chat = udB.get_key("BLACKLIST_CHATS") or []
- if chat_id not in chat:
- chat.append(chat_id)
- return udB.set_key("BLACKLIST_CHATS", chat)
-
-
-def rem_black_chat(chat_id):
- chat = udB.get_key("BLACKLIST_CHATS") or []
- if chat_id in chat:
- chat.remove(chat_id)
- return udB.set_key("BLACKLIST_CHATS", chat)
diff --git a/pyUltroid/dB/blacklist_db.py b/pyUltroid/dB/blacklist_db.py
deleted file mode 100644
index f7d9d24098..0000000000
--- a/pyUltroid/dB/blacklist_db.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_stuff():
- return udB.get_key("BLACKLIST_DB") or {}
-
-
-def add_blacklist(chat, word):
- ok = get_stuff()
- if ok.get(chat):
- for z in word.split():
- if z not in ok[chat]:
- ok[chat].append(z)
- else:
- ok.update({chat: [word]})
- return udB.set_key("BLACKLIST_DB", ok)
-
-
-def rem_blacklist(chat, word):
- ok = get_stuff()
- if ok.get(chat) and word in ok[chat]:
- ok[chat].remove(word)
- return udB.set_key("BLACKLIST_DB", ok)
-
-
-def list_blacklist(chat):
- ok = get_stuff()
- if ok.get(chat):
- txt = "".join(f"👉`{z}`\n" for z in ok[chat])
- if txt:
- return txt
-
-
-def get_blacklist(chat):
- ok = get_stuff()
- if ok.get(chat):
- return ok[chat]
diff --git a/pyUltroid/dB/echo_db.py b/pyUltroid/dB/echo_db.py
deleted file mode 100644
index 638a64ae49..0000000000
--- a/pyUltroid/dB/echo_db.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_stuff():
- return udB.get_key("ECHO") or {}
-
-
-def add_echo(chat, user):
- x = get_stuff()
- if k := x.get(int(chat)):
- if user not in k:
- k.append(int(user))
- x.update({int(chat): k})
- else:
- x.update({int(chat): [int(user)]})
- return udB.set_key("ECHO", x)
-
-
-def rem_echo(chat, user):
- x = get_stuff()
- if k := x.get(int(chat)):
- if user in k:
- k.remove(int(user))
- x.update({int(chat): k})
- return udB.set_key("ECHO", x)
-
-
-def check_echo(chat, user):
- x = get_stuff()
- if (k := x.get(int(chat))) and int(user) in k:
- return True
-
-
-def list_echo(chat):
- x = get_stuff()
- return x.get(int(chat))
diff --git a/pyUltroid/dB/filestore_db.py b/pyUltroid/dB/filestore_db.py
deleted file mode 100644
index 90ed5de099..0000000000
--- a/pyUltroid/dB/filestore_db.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_stored():
- return udB.get_key("FILE_STORE") or {}
-
-
-def store_msg(hash, msg_id):
- all = get_stored()
- all.update({hash: msg_id})
- return udB.set_key("FILE_STORE", all)
-
-
-def list_all_stored_msgs():
- all = get_stored()
- return list(all.keys())
-
-
-def get_stored_msg(hash):
- all = get_stored()
- if all.get(hash):
- return all[hash]
-
-
-def del_stored(hash):
- all = get_stored()
- all.pop(hash)
- return udB.set_key("FILE_STORE", all)
diff --git a/pyUltroid/dB/filter_db.py b/pyUltroid/dB/filter_db.py
deleted file mode 100644
index f70772affa..0000000000
--- a/pyUltroid/dB/filter_db.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_stuff():
- return udB.get_key("FILTERS") or {}
-
-
-def add_filter(chat, word, msg, media, button):
- ok = get_stuff()
- if ok.get(chat):
- ok[chat].update({word: {"msg": msg, "media": media, "button": button}})
- else:
- ok.update({chat: {word: {"msg": msg, "media": media, "button": button}}})
- udB.set_key("FILTERS", ok)
-
-
-def rem_filter(chat, word):
- ok = get_stuff()
- if ok.get(chat) and ok[chat].get(word):
- ok[chat].pop(word)
- udB.set_key("FILTERS", ok)
-
-
-def rem_all_filter(chat):
- ok = get_stuff()
- if ok.get(chat):
- ok.pop(chat)
- udB.set_key("FILTERS", ok)
-
-
-def get_filter(chat):
- ok = get_stuff()
- if ok.get(chat):
- return ok[chat]
-
-
-def list_filter(chat):
- ok = get_stuff()
- if ok.get(chat):
- return "".join(f"👉 `{z}`\n" for z in ok[chat])
diff --git a/pyUltroid/dB/forcesub_db.py b/pyUltroid/dB/forcesub_db.py
deleted file mode 100644
index 10b271543d..0000000000
--- a/pyUltroid/dB/forcesub_db.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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
-# .
-
-
-from .. import udB
-
-
-def get_chats():
- return udB.get_key("FORCESUB") or {}
-
-
-def add_forcesub(chat_id, chattojoin):
- omk = get_chats()
- omk.update({chat_id: chattojoin})
- return udB.set_key("FORCESUB", omk)
-
-
-def get_forcesetting(chat_id):
- omk = get_chats()
- if chat_id in omk.keys():
- return omk[chat_id]
-
-
-def rem_forcesub(chat_id):
- omk = get_chats()
- if chat_id in omk.keys():
- try:
- del omk[chat_id]
- return udB.set_key("FORCESUB", omk)
- except KeyError:
- return False
diff --git a/pyUltroid/dB/gban_mute_db.py b/pyUltroid/dB/gban_mute_db.py
deleted file mode 100644
index 0cd8a8dc99..0000000000
--- a/pyUltroid/dB/gban_mute_db.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def list_gbanned():
- return udB.get_key("GBAN") or {}
-
-
-def gban(user, reason):
- ok = list_gbanned()
- ok.update({int(user): reason or "No Reason. "})
- return udB.set_key("GBAN", ok)
-
-
-def ungban(user):
- ok = list_gbanned()
- if ok.get(int(user)):
- del ok[int(user)]
- return udB.set_key("GBAN", ok)
-
-
-def is_gbanned(user):
- ok = list_gbanned()
- if ok.get(int(user)):
- return ok[int(user)]
-
-
-def gmute(user):
- ok = list_gmuted()
- ok.append(int(user))
- return udB.set_key("GMUTE", ok)
-
-
-def ungmute(user):
- ok = list_gmuted()
- if user in ok:
- ok.remove(int(user))
- return udB.set_key("GMUTE", ok)
-
-
-def is_gmuted(user):
- return int(user) in list_gmuted()
-
-
-def list_gmuted():
- return udB.get_key("GMUTE") or []
diff --git a/pyUltroid/dB/greetings_db.py b/pyUltroid/dB/greetings_db.py
deleted file mode 100644
index 37d3bf6d45..0000000000
--- a/pyUltroid/dB/greetings_db.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_stuff(key=None):
- return udB.get_key(key) or {}
-
-
-def add_welcome(chat, msg, media, button):
- ok = get_stuff("WELCOME")
- ok.update({chat: {"welcome": msg, "media": media, "button": button}})
- return udB.set_key("WELCOME", ok)
-
-
-def get_welcome(chat):
- ok = get_stuff("WELCOME")
- return ok.get(chat)
-
-
-def delete_welcome(chat):
- ok = get_stuff("WELCOME")
- if ok.get(chat):
- ok.pop(chat)
- return udB.set_key("WELCOME", ok)
-
-
-def add_goodbye(chat, msg, media, button):
- ok = get_stuff("GOODBYE")
- ok.update({chat: {"goodbye": msg, "media": media, "button": button}})
- return udB.set_key("GOODBYE", ok)
-
-
-def get_goodbye(chat):
- ok = get_stuff("GOODBYE")
- return ok.get(chat)
-
-
-def delete_goodbye(chat):
- ok = get_stuff("GOODBYE")
- if ok.get(chat):
- ok.pop(chat)
- return udB.set_key("GOODBYE", ok)
-
-
-def add_thanks(chat):
- x = get_stuff("THANK_MEMBERS")
- x.update({chat: True})
- return udB.set_key("THANK_MEMBERS", x)
-
-
-def remove_thanks(chat):
- x = get_stuff("THANK_MEMBERS")
- if x.get(chat):
- x.pop(chat)
- return udB.set_key("THANK_MEMBERS", x)
-
-
-def must_thank(chat):
- x = get_stuff("THANK_MEMBERS")
- return x.get(chat)
diff --git a/pyUltroid/dB/mute_db.py b/pyUltroid/dB/mute_db.py
deleted file mode 100644
index 1390d1c60c..0000000000
--- a/pyUltroid/dB/mute_db.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_muted():
- return udB.get_key("MUTE") or {}
-
-
-def mute(chat, id):
- ok = get_muted()
- if ok.get(chat):
- if id not in ok[chat]:
- ok[chat].append(id)
- else:
- ok.update({chat: [id]})
- return udB.set_key("MUTE", ok)
-
-
-def unmute(chat, id):
- ok = get_muted()
- if ok.get(chat) and id in ok[chat]:
- ok[chat].remove(id)
- return udB.set_key("MUTE", ok)
-
-
-def is_muted(chat, id):
- ok = get_muted()
- return bool(ok.get(chat) and id in ok[chat])
diff --git a/pyUltroid/dB/notes_db.py b/pyUltroid/dB/notes_db.py
deleted file mode 100644
index a2443dbb00..0000000000
--- a/pyUltroid/dB/notes_db.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_stuff():
- return udB.get_key("NOTE") or {}
-
-
-def add_note(chat, word, msg, media, button):
- ok = get_stuff()
- if ok.get(int(chat)):
- ok[int(chat)].update({word: {"msg": msg, "media": media, "button": button}})
- else:
- ok.update({int(chat): {word: {"msg": msg, "media": media, "button": button}}})
- udB.set_key("NOTE", ok)
-
-
-def rem_note(chat, word):
- ok = get_stuff()
- if ok.get(int(chat)) and ok[int(chat)].get(word):
- ok[int(chat)].pop(word)
- return udB.set_key("NOTE", ok)
-
-
-def rem_all_note(chat):
- ok = get_stuff()
- if ok.get(int(chat)):
- ok.pop(int(chat))
- return udB.set_key("NOTE", ok)
-
-
-def get_notes(chat, word):
- ok = get_stuff()
- if ok.get(int(chat)) and ok[int(chat)].get(word):
- return ok[int(chat)][word]
-
-
-def list_note(chat):
- ok = get_stuff()
- if ok.get(int(chat)):
- return "".join(f"👉 #{z}\n" for z in ok[chat])
diff --git a/pyUltroid/dB/nsfw_db.py b/pyUltroid/dB/nsfw_db.py
deleted file mode 100644
index 9a157da368..0000000000
--- a/pyUltroid/dB/nsfw_db.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# 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
-# .
-
-
-from .. import udB
-
-
-def get_stuff(key="NSFW"):
- return udB.get_key(key) or {}
-
-
-def nsfw_chat(chat, action):
- x = get_stuff()
- x.update({chat: action})
- return udB.set_key("NSFW", x)
-
-
-def rem_nsfw(chat):
- x = get_stuff()
- if x.get(chat):
- x.pop(chat)
- return udB.set_key("NSFW", x)
-
-
-def is_nsfw(chat):
- x = get_stuff()
- if x.get(chat):
- return x[chat]
-
-
-def profan_chat(chat, action):
- x = get_stuff("PROFANITY")
- x.update({chat: action})
- return udB.set_key("PROFANITY", x)
-
-
-def rem_profan(chat):
- x = get_stuff("PROFANITY")
- if x.get(chat):
- x.pop(chat)
- return udB.set_key("PROFANITY", x)
-
-
-def is_profan(chat):
- x = get_stuff("PROFANITY")
- if x.get(chat):
- return x[chat]
diff --git a/pyUltroid/dB/snips_db.py b/pyUltroid/dB/snips_db.py
deleted file mode 100644
index 1d945f72c6..0000000000
--- a/pyUltroid/dB/snips_db.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_all_snips():
- return udB.get_key("SNIP") or {}
-
-
-def add_snip(word, msg, media, button):
- ok = get_all_snips()
- ok.update({word: {"msg": msg, "media": media, "button": button}})
- udB.set_key("SNIP", ok)
-
-
-def rem_snip(word):
- ok = get_all_snips()
- if ok.get(word):
- ok.pop(word)
- udB.set_key("SNIP", ok)
-
-
-def get_snips(word):
- ok = get_all_snips()
- if ok.get(word):
- return ok[word]
- return False
-
-
-def list_snip():
- return "".join(f"👉 ${z}\n" for z in get_all_snips())
diff --git a/pyUltroid/dB/warn_db.py b/pyUltroid/dB/warn_db.py
deleted file mode 100644
index 622f986435..0000000000
--- a/pyUltroid/dB/warn_db.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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
-# .
-
-from .. import udB
-
-
-def get_stuff():
- return udB.get_key("WARNS") or {}
-
-
-def add_warn(chat, user, count, reason):
- x = get_stuff()
- try:
- x[chat].update({user: [count, reason]})
- except BaseException:
- x.update({chat: {user: [count, reason]}})
- return udB.set_key("WARNS", x)
-
-
-def warns(chat, user):
- x = get_stuff()
- try:
- count, reason = x[chat][user][0], x[chat][user][1]
- return count, reason
- except BaseException:
- return 0, None
-
-
-def reset_warn(chat, user):
- x = get_stuff()
- try:
- x[chat].pop(user)
- return udB.set_key("WARNS", x)
- except BaseException:
- return
diff --git a/pyUltroid/database/base.py b/pyUltroid/database/base.py
new file mode 100644
index 0000000000..5209162b7d
--- /dev/null
+++ b/pyUltroid/database/base.py
@@ -0,0 +1,59 @@
+import ast
+
+
+class BaseDatabase:
+ def __init__(self, *args, **kwargs):
+ self._cache = {}
+
+ def get_key(self, key):
+ if key in self._cache:
+ return self._cache[key]
+ value = self._get_data(key)
+ self._cache.update({key: value})
+ return value
+
+ def re_cache(self):
+ self._cache.clear()
+ for key in self.keys():
+ self._cache.update({key: self.get_key(key)})
+
+ def ping(self):
+ return 1
+
+ @property
+ def usage(self):
+ return 0
+
+ def keys(self):
+ return []
+
+ def del_key(self, key):
+ if key in self._cache:
+ del self._cache[key]
+ self.delete(key)
+ return True
+
+ def _get_data(self, key=None, data=None):
+ if key:
+ data = self.get(str(key))
+ if data and isinstance(data, str):
+ try:
+ data = ast.literal_eval(data)
+ except BaseException:
+ pass
+ return data
+
+ def set_key(self, key, value, cache_only=False):
+ value = self._get_data(data=value)
+ self._cache[key] = value
+ if cache_only:
+ return
+ return self.set(str(key), str(value))
+
+ def rename(self, key1, key2):
+ _ = self.get_key(key1)
+ if _:
+ self.del_key(key1)
+ self.set_key(key2, _)
+ return 0
+ return 1
diff --git a/pyUltroid/database/mongo.py b/pyUltroid/database/mongo.py
new file mode 100644
index 0000000000..a701a73ddc
--- /dev/null
+++ b/pyUltroid/database/mongo.py
@@ -0,0 +1,46 @@
+from .base import BaseDatabase
+from pymongo import MongoClient
+
+
+class MongoDB(BaseDatabase):
+ def __init__(self, key, dbname="UltroidDB"):
+ self.dB = MongoClient(key, serverSelectionTimeoutMS=5000)
+ self.db = self.dB[dbname]
+ super().__init__()
+
+ def __repr__(self):
+ return f""
+
+ @property
+ def name(self):
+ return "Mongo"
+
+ @property
+ def usage(self):
+ return self.db.command("dbstats")["dataSize"]
+
+ def ping(self):
+ if self.dB.server_info():
+ return True
+
+ def keys(self):
+ return self.db.list_collection_names()
+
+ def set(self, key, value):
+ if key in self.keys():
+ self.db[key].replace_one({"_id": key}, {"value": str(value)})
+ else:
+ self.db[key].insert_one({"_id": key, "value": str(value)})
+ return True
+
+ def delete(self, key):
+ self.db.drop_collection(key)
+
+ def get(self, key):
+ if x := self.db[key].find_one({"_id": key}):
+ return x["value"]
+
+ def flushall(self):
+ self.dB.drop_database("UltroidDB")
+ self._cache.clear()
+ return True
diff --git a/pyUltroid/database/redis.py b/pyUltroid/database/redis.py
new file mode 100644
index 0000000000..1e27dcc7d1
--- /dev/null
+++ b/pyUltroid/database/redis.py
@@ -0,0 +1,71 @@
+from .base import BaseDatabase
+from redis import Redis
+import os
+from logging import getLogger
+from typing import Optional
+
+logger = getLogger(__name__)
+
+
+class RedisDB(BaseDatabase):
+ def __init__(
+ self,
+ host="localhost",
+ port=6379,
+ password="",
+ platform="",
+ client: Optional[Redis] = None,
+ *args,
+ **kwargs,
+ ):
+ if not client:
+ self.db = self._setup_client(host, port, password, platform, **kwargs)
+ else:
+ self.db = client
+ self.set = self.db.set
+ self.get = self.db.get
+ self.keys = self.db.keys
+ self.delete = self.db.delete
+ super().__init__()
+
+ @property
+ def name(self):
+ return "Redis"
+
+ @property
+ def usage(self):
+ return sum(self.db.memory_usage(x) for x in self.keys())
+
+ def _setup_client(self, host, port, password, platform, **kwargs):
+ if host and ":" in host:
+ spli_ = host.split(":")
+ host = spli_[0]
+ port = int(spli_[-1])
+ if host.startswith("http"):
+ logger.error("Your REDIS_URI should not start with http !")
+ import sys
+
+ sys.exit()
+ elif not host or not port:
+ logger.error("Port Number not found")
+ import sys
+
+ sys.exit()
+
+ kwargs["host"] = host
+ kwargs["password"] = password
+ kwargs["port"] = port
+
+ if platform.lower() == "qovery" and not host:
+ var, hash_, host, password = "", "", "", ""
+ for vars_ in os.environ:
+ if vars_.startswith("QOVERY_REDIS_") and vars.endswith("_HOST"):
+ var = vars_
+ if var:
+ hash_ = var.split("_", maxsplit=2)[1].split("_")[0]
+ if hash:
+ kwargs["host"] = os.environ.get(f"QOVERY_REDIS_{hash_}_HOST")
+ kwargs["port"] = os.environ.get(f"QOVERY_REDIS_{hash_}_PORT")
+ kwargs["password"] = os.environ.get(f"QOVERY_REDIS_{hash_}_PASSWORD")
+
+ return Redis(**kwargs)
diff --git a/pyUltroid/exceptions.py b/pyUltroid/exceptions.py
index bdba015f0e..76c56d582e 100644
--- a/pyUltroid/exceptions.py
+++ b/pyUltroid/exceptions.py
@@ -10,13 +10,10 @@
"""
-class pyUltroidError(Exception):
- ...
+class pyUltroidError(Exception): ...
-class DependencyMissingError(ImportError):
- ...
+class DependencyMissingError(ImportError): ...
-class RunningAsFunctionLibError(pyUltroidError):
- ...
+class RunningAsFunctionLibError(pyUltroidError): ...
diff --git a/pyUltroid/fns/FastTelethon.py b/pyUltroid/fns/FastTelethon.py
index d400a20b45..4140c06b83 100644
--- a/pyUltroid/fns/FastTelethon.py
+++ b/pyUltroid/fns/FastTelethon.py
@@ -173,7 +173,7 @@ async def _cleanup(self) -> None:
def _get_connection_count(
file_size: int,
) -> int:
- full_size = 100 * (1024 ** 2)
+ full_size = 100 * (1024**2)
if file_size > full_size:
return 20
return math.ceil((file_size / full_size) * 20)
@@ -283,7 +283,7 @@ async def init_upload(
connection_count = connection_count or self._get_connection_count(file_size)
part_size = (part_size_kb or utils.get_appropriated_part_size(file_size)) * 1024
part_count = (file_size + part_size - 1) // part_size
- is_large = file_size > 10 * (1024 ** 2)
+ is_large = file_size > 10 * (1024**2)
await self._init_upload(connection_count, file_id, part_count, is_large)
return part_size, part_count, is_large
diff --git a/pyUltroid/fns/helper.py b/pyUltroid/fns/helper.py
index 105208bd13..74df2cda39 100644
--- a/pyUltroid/fns/helper.py
+++ b/pyUltroid/fns/helper.py
@@ -14,7 +14,8 @@
from traceback import format_exc
from urllib.parse import unquote
from urllib.request import urlretrieve
-
+from PIL import Image
+import requests
from .. import run_as_module
if run_as_module:
@@ -351,49 +352,6 @@ async def downloader(filename, file, event, taime, msg):
# @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"""
@@ -617,3 +575,45 @@ async def shutdown(ult):
)
else:
sys.exit()
+
+
+def resize_photo_sticker(photo):
+ """Resize the given photo to 512x512 (for creating telegram sticker)."""
+ image = Image.open(photo)
+ if (image.width and image.height) < 512:
+ size1 = image.width
+ size2 = image.height
+ if image.width > image.height:
+ scale = 512 / size1
+ size1new = 512
+ size2new = size2 * scale
+ else:
+ scale = 512 / size2
+ size1new = size1 * scale
+ size2new = 512
+ size1new = math.floor(size1new)
+ size2new = math.floor(size2new)
+ sizenew = (size1new, size2new)
+ image = image.resize(sizenew)
+ else:
+ maxsize = (512, 512)
+ image.thumbnail(maxsize)
+ return image
+
+
+def fetch_sync(url, re_json=False, evaluate=None, method="GET", *args, **kwargs):
+ methods = {"POST": requests.post, "HEAD": requests.head, "GET": requests.get}
+ method = "POST" if kwargs.pop("post", False) else "GET"
+ output = requests.request(method, url, *args, **kwargs)
+
+ if callable(evaluate):
+ return evaluate(output)
+ elif re_json:
+ # type: ignore
+ if "application/json" in output.headers.get("content-type", ""):
+ return output.json()
+ return output.text
+ return output.content
+
+
+async_searcher = fetch = run_async(fetch_sync)
diff --git a/pyUltroid/fns/misc.py b/pyUltroid/fns/misc.py
index 85369b1b60..4cb8fa1a67 100644
--- a/pyUltroid/fns/misc.py
+++ b/pyUltroid/fns/misc.py
@@ -62,6 +62,7 @@
uploader = CatboxUploader()
+
async def randomchannel(
tochat, channel, range1, range2, caption=None, client=ultroid_bot
):
@@ -195,7 +196,7 @@ async def unsplashsearch(query, limit=None, shuf=True):
all_ = res.find_all("img", srcset=re.compile("images.unsplash.com/photo"))
if shuf:
shuffle(all_)
- return list(map(lambda e: e['src'], all_[:limit]))
+ return list(map(lambda e: e["src"], all_[:limit]))
# ---------------- Random User Gen ----------------
diff --git a/pyUltroid/fns/tools.py b/pyUltroid/fns/tools.py
index 3c23f21864..ca7a0081bf 100644
--- a/pyUltroid/fns/tools.py
+++ b/pyUltroid/fns/tools.py
@@ -39,8 +39,6 @@
from telethon import Button
from telethon.tl.types import DocumentAttributeAudio, DocumentAttributeVideo
-if run_as_module:
- from ..dB.filestore_db import get_stored_msg, store_msg
try:
import numpy as np
@@ -118,7 +116,7 @@ async def metadata(file):
raise DependencyMissingError(
f"'{_}' is not installed!\nInstall it to use this command."
)
-
+
data = {}
_info = json.loads(out)["media"]
if not _info:
@@ -391,61 +389,48 @@ def make_logo(imgpath, text, funt, **args):
async def get_paste(data: str, extension: str = "txt"):
try:
url = "https://spaceb.in/api/"
- res = await async_searcher(url, json={"content": data, "extension": extension}, post=True, re_json=True)
+ res = await async_searcher(
+ url, json={"content": data, "extension": extension}, post=True, re_json=True
+ )
return True, {
"link": f"https://spaceb.in/{res['payload']['id']}",
- "raw": f"https://spaceb.in/{res['payload']['id']}/raw"
+ "raw": f"https://spaceb.in/{res['payload']['id']}/raw",
}
except Exception:
try:
url = "https://dpaste.org/api/"
data = {
- 'format': 'json',
- 'content': data.encode('utf-8'),
- 'lexer': extension,
- 'expires': '604800', # expire in week
+ "format": "json",
+ "content": data.encode("utf-8"),
+ "lexer": extension,
+ "expires": "604800", # expire in week
}
res = await async_searcher(url, data=data, post=True, re_json=True)
- return True, {
- "link": res["url"],
- "raw": f'{res["url"]}/raw'
- }
+ return True, {"link": res["url"], "raw": f'{res["url"]}/raw'}
except Exception as e:
LOGS.info(e)
- return None, {
- "link": None,
- "raw": None,
- "error": str(e)
- }
+ return None, {"link": None, "raw": None, "error": str(e)}
+
# https://stackoverflow.com/a/74563494
async def get_google_images(query):
"""Get image results from Google Custom Search API.
-
+
Args:
query (str): Search query string
-
+
Returns:
list: List of dicts containing image info (title, link, source, thumbnail, original)
"""
LOGS.info(f"Searching Google Images for: {query}")
-
+
# Google Custom Search API credentials
google_keys = [
- {
- "key": "AIzaSyAj75v6vHWLJdJaYcj44tLz7bdsrh2g7Y0",
- "cx": "712a54749d99a449e"
- },
- {
- "key": "AIzaSyDFQQwPLCzcJ9FDao-B7zDusBxk8GoZ0HY",
- "cx": "001bbd139705f44a6"
- },
- {
- "key": "AIzaSyD0sRNZUa8-0kq9LAREDAFKLNO1HPmikRU",
- "cx": "4717c609c54e24250"
- }
+ {"key": "AIzaSyAj75v6vHWLJdJaYcj44tLz7bdsrh2g7Y0", "cx": "712a54749d99a449e"},
+ {"key": "AIzaSyDFQQwPLCzcJ9FDao-B7zDusBxk8GoZ0HY", "cx": "001bbd139705f44a6"},
+ {"key": "AIzaSyD0sRNZUa8-0kq9LAREDAFKLNO1HPmikRU", "cx": "4717c609c54e24250"},
]
key_index = random.randint(0, len(google_keys) - 1)
GOOGLE_API_KEY = google_keys[key_index]["key"]
@@ -460,35 +445,39 @@ async def get_google_images(query):
"&searchType=image"
"&num=10" # Number of results
)
-
+
# Make API request
response = await async_searcher(url, re_json=True)
print("response")
if not response or "items" not in response:
LOGS.error("No results from Google Custom Search API")
return []
-
+
# Process results
google_images = []
for item in response["items"]:
try:
- google_images.append({
- "title": item.get("title", ""),
- "link": item.get("contextLink", ""), # Page containing image
- "source": item.get("displayLink", ""),
- "thumbnail": item.get("image", {}).get("thumbnailLink", item["link"]),
- "original": item["link"] # Original image URL
- })
+ google_images.append(
+ {
+ "title": item.get("title", ""),
+ "link": item.get("contextLink", ""), # Page containing image
+ "source": item.get("displayLink", ""),
+ "thumbnail": item.get("image", {}).get(
+ "thumbnailLink", item["link"]
+ ),
+ "original": item["link"], # Original image URL
+ }
+ )
except Exception as e:
LOGS.warning(f"Failed to process image result: {str(e)}")
continue
-
+
# Randomize results order
random.shuffle(google_images)
-
+
LOGS.info(f"Found {len(google_images)} images for query: {query}")
return google_images
-
+
except Exception as e:
LOGS.exception(f"Error in get_google_images: {str(e)}")
return []
@@ -507,6 +496,7 @@ async def get_chatbot_reply(message):
except Exception:
LOGS.info(f"**ERROR:**`{format_exc()}`")
+
def check_filename(filroid):
if os.path.exists(filroid):
no = 1
@@ -676,37 +666,6 @@ async def Carbon(
return file
-async def get_file_link(msg):
- from .. import udB
-
- msg_id = await msg.forward_to(udB.get_key("LOG_CHANNEL"))
- await msg_id.reply(
- "**Message has been stored to generate a shareable link. Do not delete it.**"
- )
- msg_id = msg_id.id
- msg_hash = secrets.token_hex(nbytes=8)
- store_msg(msg_hash, msg_id)
- return msg_hash
-
-
-async def get_stored_file(event, hash):
- from .. import udB, asst
-
- msg_id = get_stored_msg(hash)
- if not msg_id:
- return
- try:
- msg = await asst.get_messages(udB.get_key("LOG_CHANNEL"), ids=msg_id)
- except Exception as er:
- LOGS.warning(f"FileStore, Error: {er}")
- return
- if not msg_id:
- return await asst.send_message(
- event.chat_id, "__Message was deleted by owner!__", reply_to=event.id
- )
- await asst.send_message(event.chat_id, msg.text, file=msg.media, reply_to=event.id)
-
-
def translate(text, lang_tgt="en", lang_src="auto", timeout=60, detect=False):
pattern = r'(?s)class="(?:t0|result-container)">(.*?)<'
escaped_text = quote(text.encode("utf8"))
@@ -724,7 +683,6 @@ def translate(text, lang_tgt="en", lang_src="auto", timeout=60, detect=False):
return (text, None) if detect else text
-
def cmd_regex_replace(cmd):
return (
cmd.replace("$", "")
@@ -744,8 +702,7 @@ def cmd_regex_replace(cmd):
# ------------------------#
-class LottieException(Exception):
- ...
+class LottieException(Exception): ...
class TgConverter:
@@ -762,7 +719,7 @@ async def animated_sticker(file, out_path="sticker.tgs", throw=False, remove=Fal
)
else:
er, out = await bash(f"lottie_convert.py '{file}' '{out_path}'")
-
+
if er:
LOGS.error(f"Error in animated_sticker conversion: {er}")
if throw:
@@ -806,7 +763,7 @@ def resize_photo_sticker(photo):
try:
image = Image.open(photo)
original_size = (image.width, image.height)
-
+
if (image.width and image.height) < 512:
size1 = image.width
size2 = image.height
@@ -825,7 +782,7 @@ def resize_photo_sticker(photo):
else:
maxsize = (512, 512)
image.thumbnail(maxsize)
-
+
LOGS.info(f"Resized image from {original_size} to {image.size}")
return image
except Exception as e:
@@ -839,7 +796,9 @@ async def ffmpeg_convert(input_, output, remove=False):
input_, name=output[:-5], remove=remove
)
if output.endswith(".gif"):
- out, er = await bash(f"ffmpeg -i '{input_}' -an -sn -c:v copy '{output}.mp4' -y")
+ out, er = await bash(
+ f"ffmpeg -i '{input_}' -an -sn -c:v copy '{output}.mp4' -y"
+ )
LOGS.info(f"FFmpeg output: {out}, Error: {er}")
else:
out, er = await bash(f"ffmpeg -i '{input_}' '{output}' -y")
@@ -856,7 +815,7 @@ async def create_webm(file, name="video", remove=False):
_ = await metadata(file)
name += ".webm"
h, w = _["height"], _["width"]
-
+
if h == w and h != 512:
h, w = 512, 512
if h != 512 or w != 512:
@@ -864,19 +823,19 @@ async def create_webm(file, name="video", remove=False):
h, w = 512, -1
if w > h:
h, w = -1, 512
-
+
await bash(
f'ffmpeg -i "{file}" -preset fast -an -to 00:00:03 -crf 30 -bufsize 256k -b:v {_["bitrate"]} -vf "scale={w}:{h},fps=30" -c:v libvpx-vp9 "{name}" -y'
)
-
+
if remove and os.path.exists(file):
os.remove(file)
LOGS.info(f"Removed original file: {file}")
-
+
if os.path.exists(name):
LOGS.info(f"Successfully created webm: {name}")
return name
-
+
LOGS.error(f"Webm creation failed - output file not created: {name}")
return None
except Exception as e:
@@ -891,37 +850,39 @@ def to_image(input_, name, remove=False):
if not input_:
LOGS.error("Input file is None")
return None
-
+
if not os.path.exists(input_):
LOGS.error(f"Input file does not exist: {input_}")
return None
-
+
try:
import cv2
except ImportError:
- raise DependencyMissingError("This function needs 'cv2' to be installed.")
-
+ raise DependencyMissingError(
+ "This function needs 'cv2' to be installed."
+ )
+
img = cv2.VideoCapture(input_)
success, frame = img.read()
-
+
if not success:
LOGS.error(f"Failed to read frame from {input_}")
return None
-
+
cv2.imwrite(name, frame)
img.release()
-
+
if not os.path.exists(name):
LOGS.error(f"Failed to save image: {name}")
return None
-
+
if remove and os.path.exists(input_):
os.remove(input_)
LOGS.info(f"Removed original file: {input_}")
-
+
LOGS.info(f"Successfully converted to image: {name}")
return name
-
+
except Exception as e:
LOGS.exception(f"Error in to_image conversion: {str(e)}")
return None
@@ -936,11 +897,11 @@ async def convert(
):
"""Convert between different file formats."""
LOGS.info(f"Converting {input_file} to {convert_to or allowed_formats}")
-
+
if not input_file:
LOGS.error("Input file is None")
return None
-
+
if not os.path.exists(input_file):
LOGS.error(f"Input file does not exist: {input_file}")
return None
@@ -977,8 +938,10 @@ def recycle_type(exte):
input_file, convert_to="gif", remove_old=remove_old
)
if gif_file:
- return await TgConverter.create_webm(gif_file, outname, remove=True)
-
+ return await TgConverter.create_webm(
+ gif_file, outname, remove=True
+ )
+
# Json -> Tgs
elif ext == "json":
if recycle_type("tgs"):
@@ -986,7 +949,7 @@ def recycle_type(exte):
return await TgConverter.animated_sticker(
input_file, name, remove=remove_old
)
-
+
# Video to Something
elif ext in ["webm", "mp4", "gif"]:
for exte in ["webm", "mp4", "gif"]:
@@ -997,14 +960,16 @@ def recycle_type(exte):
)
if result:
return result
-
+
for exte in ["png", "jpg", "jpeg", "webp"]:
if recycle_type(exte):
name = outname + "." + exte
- result = TgConverter.to_image(input_file, name, remove=remove_old)
+ result = TgConverter.to_image(
+ input_file, name, remove=remove_old
+ )
if result:
return result
-
+
# Image to Something
elif ext in ["jpg", "jpeg", "png", "webp"]:
for extn in ["png", "webp", "ico"]:
@@ -1020,7 +985,7 @@ def recycle_type(exte):
except Exception as e:
LOGS.error(f"Failed to convert image to {extn}: {str(e)}")
continue
-
+
for extn in ["webm", "gif", "mp4"]:
if recycle_type(extn):
name = outname + "." + extn
@@ -1038,10 +1003,12 @@ def recycle_type(exte):
return await TgConverter.ffmpeg_convert(
input_file, name, remove=remove_old
)
-
- LOGS.error(f"No valid conversion found for {input_file} to {convert_to or allowed_formats}")
+
+ LOGS.error(
+ f"No valid conversion found for {input_file} to {convert_to or allowed_formats}"
+ )
return None
-
+
except Exception as e:
LOGS.exception(f"Error in convert: {str(e)}")
return None
@@ -1081,13 +1048,16 @@ def safe_load(file, *args, **kwargs):
def get_chat_and_msgid(link):
- matches = re.findall("https:\\/\\/t\\.me\\/(c\\/|)(.*)\\/(.*)", link)
- if not matches:
- return None, None
- _, chat, msg_id = matches[0]
- if chat.isdigit():
- chat = int("-100" + chat)
- return chat, int(msg_id)
+ m = re.findall(r"t\.me\/(c\/)?([^\/]+)\/(\d+)", link)
+ if m:
+ is_channel, chat, msg_id = m[0]
+ if is_channel:
+ chat = int("-100" + chat)
+ return chat, int(msg_id)
+
+ m = re.findall(r"user_id=(\d+)&message_id=(\d+)", link)
+ if m:
+ return int(m[0][0]), int(m[0][1])
# --------- END --------- #
diff --git a/pyUltroid/loader.py b/pyUltroid/loader.py
index a5b4c52580..a7970b7e8d 100644
--- a/pyUltroid/loader.py
+++ b/pyUltroid/loader.py
@@ -75,3 +75,34 @@ def load(
if func == import_module:
plugin = plugin.split(".")[-1]
after_load(self, modl, plugin_name=plugin)
+
+ def load_single_plugin(self, plugin_path, func=import_module, after_load=None):
+ """Load a single plugin file"""
+ try:
+ if not os.path.exists(plugin_path):
+ self._logger.error(f"Plugin file not found: {plugin_path}")
+ return False
+
+ plugin_name = os.path.basename(plugin_path).replace(".py", "")
+
+ if func == import_module:
+ # Convert file path to module path
+ plugin_module = str(plugin_path).replace(".py", "").replace("/", ".").replace("\\", ".")
+ modl = func(plugin_module)
+ else:
+ modl = func(plugin_path)
+
+ self._logger.info(f"Successfully loaded plugin: {plugin_name}")
+
+ if callable(after_load):
+ after_load(self, modl, plugin_name=plugin_name)
+
+ return True
+
+ except ModuleNotFoundError as er:
+ self._logger.error(f"{plugin_path}: '{er.name}' not installed!")
+ return False
+ except Exception as exc:
+ self._logger.error(f"pyUltroid - {self.key} - ERROR - {plugin_path}")
+ self._logger.exception(exc)
+ return False
diff --git a/pyUltroid/scripts/redis.py b/pyUltroid/scripts/redis.py
new file mode 100644
index 0000000000..0dc5d3a1b5
--- /dev/null
+++ b/pyUltroid/scripts/redis.py
@@ -0,0 +1,31 @@
+import asyncio
+from redis import Redis
+from redis.exceptions import ConnectionError
+
+
+def connect_localhost_redis():
+ try:
+ redis = Redis(host="localhost", port=6379, db=0, decode_responses=True)
+ redis.ping()
+ return redis
+ except ConnectionError as er:
+ return False
+
+
+async def is_redis_server_installed():
+ proc = await asyncio.create_subprocess_exec("redis-server", "--version")
+ stdout, stderr = await proc.communicate()
+ return proc.returncode == 0
+
+
+async def start_redis_server():
+ proc = await asyncio.create_subprocess_exec("redis-server", "--daemonize yes")
+ stdout, stderr = await proc.communicate()
+ await asyncio.sleep(2) # Wait for server to start
+ return proc.returncode == 0
+
+
+async def stop_redis_server():
+ proc = await asyncio.create_subprocess_exec("redis-cli", "shutdown")
+ stdout, stderr = await proc.communicate()
+ return proc.returncode == 0
diff --git a/pyUltroid/scripts/webapp.py b/pyUltroid/scripts/webapp.py
new file mode 100644
index 0000000000..448e85f979
--- /dev/null
+++ b/pyUltroid/scripts/webapp.py
@@ -0,0 +1,107 @@
+import aiohttp
+import zipfile
+import os
+import logging
+from pathlib import Path
+from pyUltroid.state_config import temp_config_store
+import shutil
+
+
+async def fetch_recent_release():
+ """
+ Fetch the most recent release from GitHub, download the ultroid-dist.zip,
+ and extract it to the webapp directory.
+ Skip download if the same version is already installed.
+ """
+ try:
+ # Configuration
+ repo = "TeamUltroid/webapp"
+ zip_filename = "ultroid-dist.zip"
+ webapp_path = Path("resources/webapp") # Adjust this path as needed
+
+ # Create webapp directory if it doesn't exist
+ webapp_path.mkdir(parents=True, exist_ok=True)
+
+ # Temporary file for the download
+ temp_zip = Path(f"resources/temp_{zip_filename}")
+
+ # GitHub API endpoint to get the latest release
+ api_url = f"https://api.github.com/repos/{repo}/releases/latest"
+
+ logging.info("Fetching latest webapp release info...")
+
+ async with aiohttp.ClientSession() as session:
+ # Get latest release info
+ async with session.get(api_url) as response:
+ if response.status != 200:
+ logging.error(
+ f"Failed to fetch release info: HTTP {response.status}"
+ )
+ return False
+
+ data = await response.json()
+
+ # Get the release version/tag
+ latest_version = data.get("tag_name")
+
+ # Check if we already have this version
+ current_version = temp_config_store.get("webapp_version")
+
+ if current_version == latest_version and webapp_path.exists():
+ logging.info(
+ f"Webapp already at latest version {latest_version}. Skipping download."
+ )
+ return True
+
+ # Find the ultroid-dist.zip asset
+ asset_url = None
+ for asset in data.get("assets", []):
+ if asset["name"] == zip_filename:
+ asset_url = asset["browser_download_url"]
+ break
+
+ if not asset_url:
+ logging.error(
+ f"Could not find {zip_filename} in the latest release"
+ )
+ return False
+
+ # Download the zip file
+ logging.info(
+ f"Downloading {zip_filename} (version {latest_version})..."
+ )
+ async with session.get(asset_url) as zip_response:
+ if zip_response.status != 200:
+ logging.error(
+ f"Failed to download zip: HTTP {zip_response.status}"
+ )
+ return False
+
+ with open(temp_zip, "wb") as f:
+ f.write(await zip_response.read())
+
+ # Clear existing webapp files
+ logging.info("Clearing existing webapp files...")
+ for item in webapp_path.glob("*"):
+ if item.is_dir():
+ shutil.rmtree(item)
+ else:
+ item.unlink()
+
+ # Extract the zip file
+ logging.info(f"Extracting {zip_filename} to webapp directory...")
+ with zipfile.ZipFile(temp_zip, "r") as zip_ref:
+ zip_ref.extractall(webapp_path)
+
+ # Clean up the temporary zip file
+ temp_zip.unlink()
+
+ # Save the new version in config
+ temp_config_store.set("webapp_version", latest_version)
+
+ logging.info(f"Webapp successfully updated to version {latest_version}!")
+ return True
+
+ except Exception as e:
+ logging.error(f"Error updating webapp: {str(e)}")
+ return False
diff --git a/pyUltroid/startup/BaseClient.py b/pyUltroid/startup/BaseClient.py
index 603121b52e..d51cabecf7 100644
--- a/pyUltroid/startup/BaseClient.py
+++ b/pyUltroid/startup/BaseClient.py
@@ -113,8 +113,9 @@ async def fast_uploader(self, file, **kwargs):
message = kwargs.get("message", f"Uploading {filename}...")
by_bot = self._bot
size = os.path.getsize(file)
+
# Don't show progress bar when file size is less than 5MB.
- if size < 5 * 2 ** 20:
+ if size < 5 * 2**20:
show_progress = False
if use_cache and self._cache and self._cache.get("upload_cache"):
for files in self._cache["upload_cache"]:
@@ -140,12 +141,14 @@ async def fast_uploader(self, file, **kwargs):
file=f,
filename=filename,
progress_callback=(
- lambda completed, total: self.loop.create_task(
- progress(completed, total, event, start_time, message)
+ (
+ lambda completed, total: self.loop.create_task(
+ progress(completed, total, event, start_time, message)
+ )
)
- )
- if show_progress
- else None,
+ if show_progress
+ else None
+ ),
)
cache = {
"by_bot": by_bot,
@@ -171,7 +174,7 @@ async def fast_downloader(self, file, **kwargs):
if show_progress:
event = kwargs["event"]
# Don't show progress bar when file size is less than 10MB.
- if file.size < 10 * 2 ** 20:
+ if file.size < 10 * 2**20:
show_progress = False
import mimetypes
@@ -204,12 +207,14 @@ async def fast_downloader(self, file, **kwargs):
location=file,
out=f,
progress_callback=(
- lambda completed, total: self.loop.create_task(
- progress(completed, total, event, start_time, message)
+ (
+ lambda completed, total: self.loop.create_task(
+ progress(completed, total, event, start_time, message)
+ )
)
- )
- if show_progress
- else None,
+ if show_progress
+ else None
+ ),
)
return raw_file, time.time() - start_time
diff --git a/pyUltroid/startup/__init__.py b/pyUltroid/startup/__init__.py
index 8d8063a535..a74c3955a6 100644
--- a/pyUltroid/startup/__init__.py
+++ b/pyUltroid/startup/__init__.py
@@ -10,13 +10,8 @@
import sys
from logging import INFO, WARNING, FileHandler, StreamHandler, basicConfig, getLogger
-from .. import run_as_module
from ._extra import _ask_input
-
-if run_as_module:
- from ..configs import Var
-else:
- Var = None
+from ..configs import Var
def where_hosted():
@@ -39,61 +34,54 @@ def where_hosted():
return "local"
-if run_as_module:
- from telethon import __version__
- from telethon.tl.alltlobjects import LAYER
-
- from ..version import __version__ as __pyUltroid__
- from ..version import ultroid_version
-
- file = f"ultroid{sys.argv[6]}.log" if len(sys.argv) > 6 else "ultroid.log"
+from telethon import __version__
+from telethon.tl.alltlobjects import LAYER
- if os.path.exists(file):
- os.remove(file)
+from ..version import __version__ as __pyUltroid__
+from ..version import ultroid_version
- HOSTED_ON = where_hosted()
- LOGS = getLogger("pyUltLogs")
- TelethonLogger = getLogger("Telethon")
- TelethonLogger.setLevel(INFO)
+file = f"ultroid{sys.argv[6]}.log" if len(sys.argv) > 6 else "ultroid.log"
- _, v, __ = platform.python_version_tuple()
+if os.path.exists(file):
+ os.remove(file)
- if int(v) < 10:
- from ._extra import _fix_logging
+HOSTED_ON = where_hosted()
+LOGS = getLogger("pyUltLogs")
+TelethonLogger = getLogger("Telethon")
+TelethonLogger.setLevel(INFO)
- _fix_logging(FileHandler)
+_, v, __ = platform.python_version_tuple()
- _ask_input()
+_ask_input()
- _LOG_FORMAT = "%(asctime)s | %(name)s [%(levelname)s] : %(message)s"
- basicConfig(
- format=_LOG_FORMAT,
- level=INFO,
- datefmt="%m/%d/%Y, %H:%M:%S",
- handlers=[FileHandler(file), StreamHandler()],
- )
- try:
+_LOG_FORMAT = "%(asctime)s | %(name)s [%(levelname)s] : %(message)s"
+basicConfig(
+ format=_LOG_FORMAT,
+ level=INFO,
+ datefmt="%m/%d/%Y, %H:%M:%S",
+ handlers=[FileHandler(file), StreamHandler()],
+)
+try:
+ import coloredlogs
- import coloredlogs
+ coloredlogs.install(level=None, logger=LOGS, fmt=_LOG_FORMAT)
+except ImportError:
+ pass
- coloredlogs.install(level=None, logger=LOGS, fmt=_LOG_FORMAT)
- except ImportError:
- pass
-
- LOGS.info(
- """
- -----------------------------------
- Starting Deployment
- -----------------------------------
+LOGS.info(
+ """
+ -----------------------------------
+ Starting Deployment
+ -----------------------------------
"""
- )
+)
- LOGS.info(f"Python version - {platform.python_version()}")
- LOGS.info(f"py-Ultroid Version - {__pyUltroid__}")
- LOGS.info(f"Telethon Version - {__version__} [Layer: {LAYER}]")
- LOGS.info(f"Ultroid Version - {ultroid_version} [{HOSTED_ON}]")
+LOGS.info(f"Python version - {platform.python_version()}")
+LOGS.info(f"py-Ultroid Version - {__pyUltroid__}")
+LOGS.info(f"Telethon Version - {__version__} [Layer: {LAYER}]")
+LOGS.info(f"Ultroid Version - {ultroid_version} [{HOSTED_ON}]")
- try:
- from safety.tools import *
- except ImportError:
- LOGS.error("'safety' package not found!")
+try:
+ from safety.tools import *
+except ImportError:
+ LOGS.error("'safety' package not found!")
diff --git a/pyUltroid/startup/_database.py b/pyUltroid/startup/_database.py
index f7bc274524..7a45c14a68 100644
--- a/pyUltroid/startup/_database.py
+++ b/pyUltroid/startup/_database.py
@@ -1,352 +1,86 @@
# Ultroid - UserBot
# Copyright (C) 2021-2025 TeamUltroid
#
-# This file is a part of < https://github.com/TeamUltroid/Ultroid/ >
+# This file is a part of
# PLease read the GNU Affero General Public License in
# .
-import ast
-import os
-import sys
-
-from .. import run_as_module
+import asyncio
from . import *
-
-if run_as_module:
- from ..configs import Var
-
-
-Redis = MongoClient = psycopg2 = Database = None
-if Var.REDIS_URI or Var.REDISHOST:
- try:
- from redis import Redis
- except ImportError:
- LOGS.info("Installing 'redis' for database.")
- os.system(f"{sys.executable} -m pip install -q redis hiredis")
- from redis import Redis
-elif Var.MONGO_URI:
- try:
- from pymongo import MongoClient
- except ImportError:
- LOGS.info("Installing 'pymongo' for database.")
- os.system(f"{sys.executable} -m pip install -q pymongo[srv]")
- from pymongo import MongoClient
-elif Var.DATABASE_URL:
- try:
- import psycopg2
- except ImportError:
- LOGS.info("Installing 'pyscopg2' for database.")
- os.system(f"{sys.executable} -m pip install -q psycopg2-binary")
- import psycopg2
-else:
- try:
- from localdb import Database
- except ImportError:
- LOGS.info("Using local file as database.")
- os.system(f"{sys.executable} -m pip install -q localdb.json")
- from localdb import Database
-
-# --------------------------------------------------------------------------------------------- #
-
-
-class _BaseDatabase:
- def __init__(self, *args, **kwargs):
- self._cache = {}
-
- def get_key(self, key):
- if key in self._cache:
- return self._cache[key]
- value = self._get_data(key)
- self._cache.update({key: value})
- return value
-
- def re_cache(self):
- self._cache.clear()
- for key in self.keys():
- self._cache.update({key: self.get_key(key)})
-
- def ping(self):
- return 1
-
- @property
- def usage(self):
- return 0
-
- def keys(self):
- return []
-
- def del_key(self, key):
- if key in self._cache:
- del self._cache[key]
- self.delete(key)
- return True
-
- def _get_data(self, key=None, data=None):
- if key:
- data = self.get(str(key))
- if data and isinstance(data, str):
- try:
- data = ast.literal_eval(data)
- except BaseException:
- pass
- return data
-
- def set_key(self, key, value, cache_only=False):
- value = self._get_data(data=value)
- self._cache[key] = value
- if cache_only:
- return
- return self.set(str(key), str(value))
-
- def rename(self, key1, key2):
- _ = self.get_key(key1)
- if _:
- self.del_key(key1)
- self.set_key(key2, _)
- return 0
- return 1
+from ..configs import Var
-class MongoDB(_BaseDatabase):
- def __init__(self, key, dbname="UltroidDB"):
- self.dB = MongoClient(key, serverSelectionTimeoutMS=5000)
- self.db = self.dB[dbname]
- super().__init__()
-
- def __repr__(self):
- return f""
-
- @property
- def name(self):
- return "Mongo"
-
- @property
- def usage(self):
- return self.db.command("dbstats")["dataSize"]
-
- def ping(self):
- if self.dB.server_info():
- return True
-
- def keys(self):
- return self.db.list_collection_names()
-
- def set(self, key, value):
- if key in self.keys():
- self.db[key].replace_one({"_id": key}, {"value": str(value)})
- else:
- self.db[key].insert_one({"_id": key, "value": str(value)})
- return True
-
- def delete(self, key):
- self.db.drop_collection(key)
-
- def get(self, key):
- if x := self.db[key].find_one({"_id": key}):
- return x["value"]
-
- def flushall(self):
- self.dB.drop_database("UltroidDB")
- self._cache.clear()
- return True
-
-
-# --------------------------------------------------------------------------------------------- #
-
-# Thanks to "Akash Pattnaik" / @BLUE-DEVIL1134
-# for SQL Implementation in Ultroid.
-#
-# Please use https://elephantsql.com/ !
-
+async def UltroidDB():
+ from .. import HOSTED_ON
-class SqlDB(_BaseDatabase):
- def __init__(self, url):
- self._url = url
- self._connection = None
- self._cursor = None
+ # Try Redis first if configured
+ if Var.REDIS_URI or Var.REDISHOST:
try:
- self._connection = psycopg2.connect(dsn=url)
- self._connection.autocommit = True
- self._cursor = self._connection.cursor()
- self._cursor.execute(
- "CREATE TABLE IF NOT EXISTS Ultroid (ultroidCli varchar(70))"
- )
- except Exception as error:
- LOGS.exception(error)
- LOGS.info("Invaid SQL Database")
- if self._connection:
- self._connection.close()
- sys.exit()
- super().__init__()
-
- @property
- def name(self):
- return "SQL"
+ from ..database.redis import RedisDB
- @property
- def usage(self):
- self._cursor.execute(
- "SELECT pg_size_pretty(pg_relation_size('Ultroid')) AS size"
- )
- data = self._cursor.fetchall()
- return int(data[0][0].split()[0])
-
- def keys(self):
- self._cursor.execute(
- "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'ultroid'"
- ) # case sensitive
- data = self._cursor.fetchall()
- return [_[0] for _ in data]
+ return RedisDB(
+ host=Var.REDIS_URI or Var.REDISHOST,
+ password=Var.REDIS_PASSWORD or Var.REDISPASSWORD,
+ port=Var.REDISPORT,
+ platform=HOSTED_ON,
+ decode_responses=True,
+ socket_timeout=5,
+ retry_on_timeout=True,
+ )
+ except Exception as e:
+ LOGS.warning(f"Redis connection failed: {e}")
- def get(self, variable):
+ # Try MongoDB if configured
+ if Var.MONGO_URI:
try:
- self._cursor.execute(f"SELECT {variable} FROM Ultroid")
- except psycopg2.errors.UndefinedColumn:
- return None
- data = self._cursor.fetchall()
- if not data:
- return None
- if len(data) >= 1:
- for i in data:
- if i[0]:
- return i[0]
+ from ..database.mongo import MongoDB
- def set(self, key, value):
- try:
- self._cursor.execute(f"ALTER TABLE Ultroid DROP COLUMN IF EXISTS {key}")
- except (psycopg2.errors.UndefinedColumn, psycopg2.errors.SyntaxError):
- pass
- except BaseException as er:
- LOGS.exception(er)
- self._cache.update({key: value})
- self._cursor.execute(f"ALTER TABLE Ultroid ADD {key} TEXT")
- self._cursor.execute(f"INSERT INTO Ultroid ({key}) values (%s)", (str(value),))
- return True
+ return MongoDB(Var.MONGO_URI)
+ except Exception as e:
+ LOGS.warning(f"MongoDB connection failed: {e}")
- def delete(self, key):
- try:
- self._cursor.execute(f"ALTER TABLE Ultroid DROP COLUMN {key}")
- except psycopg2.errors.UndefinedColumn:
- return False
- return True
+ # Try local Redis server
+ try:
+ from ..database.redis import RedisDB
- def flushall(self):
- self._cache.clear()
- self._cursor.execute("DROP TABLE Ultroid")
- self._cursor.execute(
- "CREATE TABLE IF NOT EXISTS Ultroid (ultroidCli varchar(70))"
+ from ..scripts.redis import (
+ is_redis_server_installed,
+ connect_localhost_redis,
+ start_redis_server,
)
- return True
-
-
-# --------------------------------------------------------------------------------------------- #
-
-class RedisDB(_BaseDatabase):
- def __init__(
- self,
- host,
- port,
- password,
- platform="",
- logger=LOGS,
- *args,
- **kwargs,
- ):
- if host and ":" in host:
- spli_ = host.split(":")
- host = spli_[0]
- port = int(spli_[-1])
- if host.startswith("http"):
- logger.error("Your REDIS_URI should not start with http !")
- import sys
+ # Check for running Redis instance first
+ if redis := connect_localhost_redis():
+ LOGS.info("Connected to running Redis server")
+ return RedisDB(client=redis)
- sys.exit()
- elif not host or not port:
- logger.error("Port Number not found")
- import sys
+ # Try to start Redis if not running
+ if not await is_redis_server_installed():
+ raise ConnectionError("Redis server is not installed")
- sys.exit()
- kwargs["host"] = host
- kwargs["password"] = password
- kwargs["port"] = port
+ if await start_redis_server():
+ LOGS.info("Started new Redis server")
+ if redis := connect_localhost_redis():
+ return RedisDB(client=redis)
+ raise ConnectionError("Failed to connect to newly started Redis server")
- if platform.lower() == "qovery" and not host:
- var, hash_, host, password = "", "", "", ""
- for vars_ in os.environ:
- if vars_.startswith("QOVERY_REDIS_") and vars.endswith("_HOST"):
- var = vars_
- if var:
- hash_ = var.split("_", maxsplit=2)[1].split("_")[0]
- if hash:
- kwargs["host"] = os.environ.get(f"QOVERY_REDIS_{hash_}_HOST")
- kwargs["port"] = os.environ.get(f"QOVERY_REDIS_{hash_}_PORT")
- kwargs["password"] = os.environ.get(f"QOVERY_REDIS_{hash_}_PASSWORD")
- self.db = Redis(**kwargs)
- self.set = self.db.set
- self.get = self.db.get
- self.keys = self.db.keys
- self.delete = self.db.delete
- super().__init__()
-
- @property
- def name(self):
- return "Redis"
-
- @property
- def usage(self):
- return sum(self.db.memory_usage(x) for x in self.keys())
-
-
-# --------------------------------------------------------------------------------------------- #
+ raise ConnectionError("Failed to start Redis server")
+ except Exception as e:
+ LOGS.warning(f"Local Redis setup failed: {e}")
-class LocalDB(_BaseDatabase):
- def __init__(self):
- self.db = Database("ultroid")
- self.get = self.db.get
- self.set = self.db.set
- self.delete = self.db.delete
- super().__init__()
-
- @property
- def name(self):
- return "LocalDB"
-
- def keys(self):
- return self._cache.keys()
-
- def __repr__(self):
- return f""
-
-
-def UltroidDB():
- _er = False
- from .. import HOSTED_ON
-
+ # Fallback to memory database
try:
- if Redis:
- return RedisDB(
- host=Var.REDIS_URI or Var.REDISHOST,
- password=Var.REDIS_PASSWORD or Var.REDISPASSWORD,
- port=Var.REDISPORT,
- platform=HOSTED_ON,
- decode_responses=True,
- socket_timeout=5,
- retry_on_timeout=True,
- )
- elif MongoClient:
- return MongoDB(Var.MONGO_URI)
- elif psycopg2:
- return SqlDB(Var.DATABASE_URL)
- else:
- LOGS.critical(
- "No DB requirement fullfilled!\nPlease install redis, mongo or sql dependencies...\nTill then using local file as database."
- )
- return LocalDB()
- except BaseException as err:
- LOGS.exception(err)
- exit()
+ from ..database.base import BaseDatabase
+
+ LOGS.critical(
+ "No database requirements fulfilled! Using memory database as fallback. "
+ "Please install Redis, MongoDB or SQL dependencies for persistent storage."
+ )
+ return BaseDatabase()
+ except Exception as e:
+ LOGS.critical(f"Failed to initialize memory database: {e}")
+ exit(1)
# --------------------------------------------------------------------------------------------- #
diff --git a/pyUltroid/startup/_extra.py b/pyUltroid/startup/_extra.py
index e71907f841..6b0baf8acd 100644
--- a/pyUltroid/startup/_extra.py
+++ b/pyUltroid/startup/_extra.py
@@ -5,20 +5,6 @@
# PLease read the GNU Affero General Public License in
# .
-# https://bugs.python.org/issue26789
-# 'open' not defined has been fixed in Python3.10
-# for other older versions, something need to be done.
-
-
-def _fix_logging(handler):
- handler._builtin_open = open
-
- def _new_open(self):
- open_func = self._builtin_open
- return open_func(self.baseFilename, self.mode)
-
- setattr(handler, "_open", _new_open)
-
def _ask_input():
# Ask for Input even on Vps and other platforms.
diff --git a/pyUltroid/startup/connections.py b/pyUltroid/startup/connections.py
index 005f199218..aac9f0d716 100644
--- a/pyUltroid/startup/connections.py
+++ b/pyUltroid/startup/connections.py
@@ -12,25 +12,19 @@
from telethon.errors.rpcerrorlist import AuthKeyDuplicatedError
from telethon.sessions.string import _STRUCT_PREFORMAT, CURRENT_VERSION, StringSession
+from logging import getLogger
from ..configs import Var
-from . import *
from .BaseClient import UltroidClient
_PYRO_FORM = {351: ">B?256sI?", 356: ">B?256sQ?", 362: ">BI?256sQ?"}
-# https://github.com/pyrogram/pyrogram/blob/master/docs/source/faq/what-are-the-ip-addresses-of-telegram-data-centers.rst
+logger = getLogger(__name__)
-DC_IPV4 = {
- 1: "149.154.175.53",
- 2: "149.154.167.51",
- 3: "149.154.175.100",
- 4: "149.154.167.91",
- 5: "91.108.56.130",
-}
+# https://github.com/pyrogram/pyrogram/blob/master/docs/source/faq/what-are-the-ip-addresses-of-telegram-data-centers.rst
-def validate_session(session, logger=LOGS, _exit=True):
+def validate_session(session, logger=logger, _exit=True):
from strings import get_string
if session:
@@ -43,6 +37,14 @@ def validate_session(session, logger=LOGS, _exit=True):
# Pyrogram Session
elif len(session) in _PYRO_FORM.keys():
+ DC_IPV4 = {
+ 1: "149.154.175.53",
+ 2: "149.154.167.51",
+ 3: "149.154.175.100",
+ 4: "149.154.167.91",
+ 5: "91.108.56.130",
+ }
+
data_ = struct.unpack(
_PYRO_FORM[len(session)],
base64.urlsafe_b64decode(session + "=" * (-len(session) % 4)),
@@ -78,7 +80,7 @@ def vc_connection(udB, ultroid_bot):
VC_SESSION = Var.VC_SESSION or udB.get_key("VC_SESSION")
if VC_SESSION and VC_SESSION != Var.SESSION:
- LOGS.info("Starting up VcClient.")
+ logger.info("Starting up VcClient.")
try:
return UltroidClient(
validate_session(VC_SESSION, _exit=False),
@@ -86,9 +88,9 @@ def vc_connection(udB, ultroid_bot):
exit_on_error=False,
)
except (AuthKeyDuplicatedError, EOFError):
- LOGS.info(get_string("py_c3"))
+ logger.info(get_string("py_c3"))
udB.del_key("VC_SESSION")
except Exception as er:
- LOGS.info("While creating Client for VC.")
- LOGS.exception(er)
+ logger.info("While creating Client for VC.")
+ logger.exception(er)
return ultroid_bot
diff --git a/pyUltroid/startup/funcs.py b/pyUltroid/startup/funcs.py
index f5338fa709..370bd8e5bb 100644
--- a/pyUltroid/startup/funcs.py
+++ b/pyUltroid/startup/funcs.py
@@ -11,6 +11,8 @@
import shutil
import time
from random import randint
+import base64
+from urllib.parse import unquote
from ..configs import Var
@@ -43,6 +45,11 @@
)
from telethon.utils import get_peer_id
from decouple import config, RepositoryEnv
+from telethon import functions
+from urllib.parse import urlparse, parse_qs
+import json
+from datetime import datetime
+import requests
from .. import LOGS, ULTConfig
from ..fns.helper import download_file, inline_mention, updater
@@ -90,6 +97,7 @@ async def autoupdate_local_database():
def update_envs():
"""Update Var. attributes to udB"""
from .. import udB
+
_envs = [*list(os.environ)]
if ".env" in os.listdir("."):
[_envs.append(_) for _ in list(RepositoryEnv(config._find_file(".")).data)]
@@ -107,11 +115,37 @@ def update_envs():
async def startup_stuff():
from .. import udB
+ # Create essential directories first - fast operation
x = ["resources/auth", "resources/downloads"]
for x in x:
if not os.path.isdir(x):
os.mkdir(x)
+ # Handle critical configuration first
+ TZ = udB.get_key("TIMEZONE")
+ if TZ and timezone:
+ try:
+ timezone(TZ)
+ os.environ["TZ"] = TZ
+ time.tzset()
+ except AttributeError as er:
+ LOGS.debug(er)
+ except BaseException:
+ LOGS.critical(
+ "Incorrect Timezone ,\nCheck Available Timezone From Here https://graph.org/Ultroid-06-18-2\nSo Time is Default UTC"
+ )
+ os.environ["TZ"] = "UTC"
+ time.tzset()
+
+ # Start background task for less critical operations
+ asyncio.create_task(_delayed_startup_operations())
+
+
+async def _delayed_startup_operations():
+ # These operations are moved to a separate function to run in background
+ from .. import udB, ULTConfig
+ from ..fns.helper import download_file
+
CT = udB.get_key("CUSTOM_THUMBNAIL")
if CT:
path = "resources/extras/thumbnail.jpg"
@@ -122,10 +156,11 @@ async def startup_stuff():
LOGS.exception(er)
elif CT is False:
ULTConfig.thumb = None
+
GT = udB.get_key("GDRIVE_AUTH_TOKEN")
if GT:
with open("resources/auth/gdrive_creds.json", "w") as t_file:
- t_file.write(GT)
+ t_file.write(json.dumps(GT))
if udB.get_key("AUTH_TOKEN"):
udB.del_key("AUTH_TOKEN")
@@ -136,21 +171,6 @@ async def startup_stuff():
with open(".megarc", "w") as mega:
mega.write(f"[Login]\nUsername = {MM}\nPassword = {MP}")
- TZ = udB.get_key("TIMEZONE")
- if TZ and timezone:
- try:
- timezone(TZ)
- os.environ["TZ"] = TZ
- time.tzset()
- except AttributeError as er:
- LOGS.debug(er)
- except BaseException:
- LOGS.critical(
- "Incorrect Timezone ,\nCheck Available Timezone From Here https://graph.org/Ultroid-06-18-2\nSo Time is Default UTC"
- )
- os.environ["TZ"] = "UTC"
- time.tzset()
-
async def autobot():
from .. import udB, ultroid_bot
@@ -221,8 +241,10 @@ async def autobot():
async def autopilot():
+ # Split into critical and non-critical operations
from .. import asst, udB, ultroid_bot
+ # Critical: Ensure LOG_CHANNEL exists
channel = udB.get_key("LOG_CHANNEL")
new_channel = None
if channel:
@@ -232,8 +254,9 @@ async def autopilot():
LOGS.exception(err)
udB.del_key("LOG_CHANNEL")
channel = None
- if not channel:
+ if not channel:
+ # Create log channel if needed - this is critical for operation
async def _save(exc):
udB._cache["LOG_CHANNEL"] = ultroid_bot.me.id
await asst.send_message(
@@ -244,6 +267,7 @@ async def _save(exc):
msg_ = "'LOG_CHANNEL' not found! Add it in order to use 'BOTMODE'"
LOGS.error(msg_)
return await _save(msg_)
+
LOGS.info("Creating a Log Channel for You!")
try:
r = await ultroid_bot(
@@ -263,12 +287,25 @@ async def _save(exc):
LOGS.info(
"Something Went Wrong , Create A Group and set its id on config var LOG_CHANNEL."
)
-
return await _save(str(er))
+
new_channel = True
chat = r.chats[0]
channel = get_peer_id(chat)
udB.set_key("LOG_CHANNEL", channel)
+
+ # Start non-critical operations in background
+ asyncio.create_task(_delayed_autopilot(channel, new_channel))
+
+ return channel
+
+
+async def _delayed_autopilot(channel, new_channel):
+ # Non-critical autopilot operations
+ from .. import asst, udB, ultroid_bot
+ from ..fns.helper import download_file
+
+ # Handle assistant bot permissions in log channel
assistant = True
try:
await ultroid_bot.get_permissions(int(channel), asst.me.username)
@@ -282,6 +319,7 @@ async def _save(exc):
except BaseException as er:
assistant = False
LOGS.exception(er)
+
if assistant and new_channel:
try:
achat = await asst.get_entity(int(channel))
@@ -289,6 +327,7 @@ async def _save(exc):
achat = None
LOGS.info("Error while getting Log channel from Assistant")
LOGS.exception(er)
+
if achat and not achat.admin_rights:
rights = ChatAdminRights(
add_admins=True,
@@ -313,18 +352,25 @@ async def _save(exc):
except BaseException as er:
LOGS.info("Error while promoting assistant in Log Channel..")
LOGS.exception(er)
- if isinstance(chat.photo, ChatPhotoEmpty):
- photo, _ = await download_file(
- "https://graph.org/file/27c6812becf6f376cbb10.jpg", "channelphoto.jpg"
- )
- ll = await ultroid_bot.upload_file(photo)
- try:
- await ultroid_bot(
- EditPhotoRequest(int(channel), InputChatUploadedPhoto(ll))
- )
- except BaseException as er:
- LOGS.exception(er)
- os.remove(photo)
+
+ # Set channel photo - low priority
+ try:
+ chat = await ultroid_bot.get_entity(int(channel))
+ if isinstance(chat.photo, ChatPhotoEmpty):
+ try:
+ photo, _ = await download_file(
+ "https://graph.org/file/27c6812becf6f376cbb10.jpg",
+ "channelphoto.jpg",
+ )
+ ll = await ultroid_bot.upload_file(photo)
+ await ultroid_bot(
+ EditPhotoRequest(int(channel), InputChatUploadedPhoto(ll))
+ )
+ os.remove(photo)
+ except BaseException as er:
+ LOGS.exception(er)
+ except Exception as e:
+ LOGS.exception(e)
# customize assistant
@@ -432,7 +478,6 @@ async def plug(plugin_channels):
LOGS.exception(er)
-
async def ready():
from .. import asst, udB, ultroid_bot
@@ -527,3 +572,123 @@ async def enable_inline(ultroid_bot, username):
await asyncio.sleep(1)
await ultroid_bot.send_message(bf, "Search")
await ultroid_bot.send_read_acknowledge(bf)
+
+
+async def user_sync_workflow():
+ from .. import udB, ultroid_bot
+ from ..configs import CENTRAL_REPO_URL, ADMIN_BOT_USERNAME
+
+ from ..state_config import temp_config_store
+
+ # Check if user data exists in temp config store
+ user_data = temp_config_store.get("X-TG-USER")
+
+ if user_data and (user_data.get("id") != ultroid_bot.uid):
+ temp_config_store.remove("X-TG-USER")
+ temp_config_store.remove(f"X-TG-INIT-DATA-{user_data['id']}")
+ temp_config_store.remove(f"X-TG-HASH-{user_data['id']}")
+
+ if user_data:
+ user_id = str(user_data["id"])
+ # Get encoded data and decode it
+ encoded_init_data = temp_config_store.get(f"X-TG-INIT-DATA-{user_id}")
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{user_id}")
+
+ # Decode the data
+ init_data = (
+ base64.b64decode(encoded_init_data.encode()).decode()
+ if encoded_init_data
+ else None
+ )
+ hash_value = (
+ base64.b64decode(encoded_hash.encode()).decode() if encoded_hash else None
+ )
+
+ return await authenticate_user_request(user_data, init_data, hash_value)
+
+ try:
+
+ url = (
+ await ultroid_bot(
+ functions.messages.RequestWebViewRequest(
+ ADMIN_BOT_USERNAME,
+ ADMIN_BOT_USERNAME,
+ platform="android",
+ from_bot_menu=False,
+ url=CENTRAL_REPO_URL,
+ )
+ )
+ ).url
+
+ # Parse the URL fragment to get webAppData
+ fragment = urlparse(url).fragment
+ params = parse_qs(fragment)
+ tg_web_data_raw = params.get("tgWebAppData", [None])[0]
+
+ # Parse the tgWebAppData parameters
+ tg_data_params = parse_qs(tg_web_data_raw) if tg_web_data_raw else {}
+
+ # Extract and parse user data
+ user_str = tg_data_params.get("user", [None])[0]
+ if not user_str:
+ raise Exception("No user data found")
+
+ user = json.loads(unquote(user_str))
+ hash_value = tg_data_params.get("hash", [None])[0]
+
+ await authenticate_user_request(user, tg_web_data_raw, hash_value)
+ except Exception as e:
+ LOGS.exception(e)
+
+
+async def authenticate_user_request(user: dict, init_data: str, hash_value: str):
+ from .. import udB, ultroid_bot
+ from ..configs import CENTRAL_REPO_URL, ADMIN_BOT_USERNAME
+
+ from ..state_config import temp_config_store
+
+ try:
+
+ # Prepare user data payload
+ user_data = {
+ "id": user["id"],
+ "first_name": user["first_name"],
+ "last_name": user.get("last_name", ""),
+ "username": user["username"],
+ "language_code": user["language_code"],
+ "photo_url": user["photo_url"],
+ "last_active": datetime.now().isoformat(),
+ "joined_at": datetime.now().isoformat(),
+ }
+
+ # Encode init data and hash using base64 before storing
+ user_id = str(user["id"])
+ encoded_init_data = (
+ base64.b64encode(init_data.encode()).decode() if init_data else ""
+ )
+ encoded_hash = (
+ base64.b64encode(hash_value.encode()).decode() if hash_value else ""
+ )
+
+ # Store with user ID as part of the key
+ temp_config_store.set(f"X-TG-INIT-DATA-{user_id}", encoded_init_data)
+ temp_config_store.set(f"X-TG-HASH-{user_id}", encoded_hash)
+ temp_config_store.set("X-TG-USER", user_data)
+
+ # Make PUT request
+ response = requests.put(
+ f"{CENTRAL_REPO_URL}/api/v1/users/{user['id']}",
+ headers={
+ "Content-Type": "application/json",
+ "x-telegram-init-data": init_data,
+ "x-telegram-hash": hash_value or "",
+ },
+ json=user_data,
+ )
+ if response.status_code == 200:
+ LOGS.info(f"User {user['id']} authenticated successfully")
+ else:
+ LOGS.error(f"User {user['id']} authentication failed")
+
+ except Exception as e:
+ LOGS.exception(e)
diff --git a/pyUltroid/startup/loader.py b/pyUltroid/startup/loader.py
index 5f49114a2e..7064568034 100644
--- a/pyUltroid/startup/loader.py
+++ b/pyUltroid/startup/loader.py
@@ -9,6 +9,10 @@
import subprocess
import sys
from shutil import rmtree
+import json
+from pathlib import Path
+import aiohttp
+import base64
from decouple import config
from git import Repo
@@ -18,13 +22,20 @@
from ..loader import Loader
from . import *
from .utils import load_addons
-
+from ..configs import CENTRAL_REPO_URL
def _after_load(loader, module, plugin_name=""):
if not module or plugin_name.startswith("_"):
return
from strings import get_help
+ # Normalize Addons plugin names by stripping last two hashes
+ normalized_name = plugin_name
+ if loader.key == "Addons" and plugin_name.count("_") >= 2:
+ normalized_name = "_".join(plugin_name.rsplit("_", 2)[:-2])
+ else:
+ normalized_name = plugin_name
+
if doc_ := get_help(plugin_name) or module.__doc__:
try:
doc = doc_.format(i=HNDLR)
@@ -35,17 +46,18 @@ def _after_load(loader, module, plugin_name=""):
if loader.key in HELP.keys():
update_cmd = HELP[loader.key]
try:
- update_cmd.update({plugin_name: doc})
+ update_cmd.update({normalized_name: doc})
except BaseException as er:
loader._logger.exception(er)
else:
try:
- HELP.update({loader.key: {plugin_name: doc}})
+ HELP.update({loader.key: {normalized_name: doc}})
except BaseException as em:
loader._logger.exception(em)
def load_other_plugins(addons=None, pmbot=None, manager=None, vcbot=None):
+ import asyncio
# for official
_exclude = udB.get_key("EXCLUDE_OFFICIAL") or config("EXCLUDE_OFFICIAL", None)
@@ -54,46 +66,293 @@ def load_other_plugins(addons=None, pmbot=None, manager=None, vcbot=None):
# "INCLUDE_ONLY" was added to reduce Big List in "EXCLUDE_OFFICIAL" Plugin
_in_only = udB.get_key("INCLUDE_ONLY") or config("INCLUDE_ONLY", None)
_in_only = _in_only.split() if _in_only else []
+
+ # Load official plugins first - these are critical for core functionality
Loader().load(include=_in_only, exclude=_exclude, after_load=_after_load)
- # for assistant
+ # List to collect all background tasks
+ background_tasks = []
+
+ # for assistant - load in background
if not USER_MODE and not udB.get_key("DISABLE_AST_PLUGINS"):
_ast_exc = ["pmbot"]
if _in_only and "games" not in _in_only:
_ast_exc.append("games")
- Loader(path="assistant").load(
- log=False, exclude=_ast_exc, after_load=_after_load
- )
- # for addons
+ # Create an async function that can be used with create_task
+ async def load_assistant():
+ Loader(path="assistant").load(
+ log=False, exclude=_ast_exc, after_load=_after_load
+ )
+
+ # Add to background tasks
+ loop = asyncio.get_event_loop()
+ background_tasks.append(loop.create_task(load_assistant()))
+
+ # for addons - prepare in background but don't block startup
if addons:
- if url := udB.get_key("ADDONS_URL"):
- subprocess.run(f"git clone -q {url} addons", shell=True)
- if os.path.exists("addons") and not os.path.exists("addons/.git"):
- rmtree("addons")
+ loop = asyncio.get_event_loop()
+ background_tasks.append(loop.create_task(setup_addons()))
+
+ if not USER_MODE:
+ # Load these in background as they're not critical
+ async def load_extra_modules():
+ # group manager
+ if manager:
+ Loader(path="assistant/manager", key="Group Manager").load()
+
+ # chat via assistant
+ if pmbot:
+ Loader(path="assistant/pmbot.py").load(log=False)
+
+ # Add to background tasks
+ loop = asyncio.get_event_loop()
+ background_tasks.append(loop.create_task(load_extra_modules()))
+
+ # vc bot - load in background
+ if vcbot and (vcClient and not vcClient.me.bot):
+
+ async def setup_vcbot():
+ try:
+ import pytgcalls # ignore: pylint
+
+ if os.path.exists("vcbot"):
+ if os.path.exists("vcbot/.git"):
+ subprocess.run("cd vcbot && git pull", shell=True)
+ else:
+ rmtree("vcbot")
+ if not os.path.exists("vcbot"):
+ subprocess.run(
+ "git clone https://github.com/TeamUltroid/VcBot vcbot",
+ shell=True,
+ )
+ try:
+ if not os.path.exists("vcbot/downloads"):
+ os.mkdir("vcbot/downloads")
+ Loader(path="vcbot", key="VCBot").load(after_load=_after_load)
+ except FileNotFoundError as e:
+ LOGS.error(f"{e} Skipping VCBot Installation.")
+ except ModuleNotFoundError:
+ LOGS.error("'pytgcalls' not installed!\nSkipping loading of VCBOT.")
+
+ # Add to background tasks
+ background_tasks.append(asyncio.create_task(setup_vcbot()))
+
+ # Return the list of background tasks in case the caller wants to await them
+ return background_tasks
+
+async def check_for_updates():
+ """Check for updates to official plugins using compute_diff endpoint."""
+ try:
+ from ..state_config import temp_config_store
+ stored_states = json.loads(temp_config_store.get("OFFICIAL_PLUGINS_STATE") or "{}")
+
+ if not stored_states:
+ LOGS.info("No stored plugin states found. Skipping update check.")
+ return []
+
+ # Get authentication data
+ user_data = temp_config_store.get("X-TG-USER")
+ if not user_data:
+ LOGS.error("No authentication data found. Please authenticate first.")
+ return []
+
+ user_id = str(user_data["id"])
+ encoded_init_data = temp_config_store.get(f"X-TG-INIT-DATA-{user_id}")
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{user_id}")
+
+ if not encoded_init_data or not encoded_hash:
+ LOGS.error("Missing authentication tokens. Please authenticate first.")
+ return []
+
+ # Decode authentication data
+ init_data = base64.b64decode(encoded_init_data.encode()).decode()
+ hash_value = base64.b64decode(encoded_hash.encode()).decode()
+
+ async with aiohttp.ClientSession() as session:
+ api_url = f"{CENTRAL_REPO_URL}/api/v1/plugins/compute_diff"
+
+ headers = {
+ "Content-Type": "application/json",
+ "X-Telegram-Init-Data": init_data,
+ "X-Telegram-Hash": hash_value
+ }
+
+ async with session.post(
+ api_url,
+ json=stored_states,
+ headers=headers
+ ) as response:
+ if response.status == 200:
+ data = await response.json()
+ updates = data.get("updates_available", [])
+
+ if updates:
+ LOGS.info(f"Found {len(updates)} plugin updates available")
+ return updates
+ else:
+ LOGS.info("All plugins are up to date")
+ return []
+ else:
+ LOGS.error(f"Failed to check for updates. Status: {response.status}")
+ return []
+ except Exception as e:
+ LOGS.error(f"Error checking for plugin updates: {str(e)}")
+ return []
+
+async def setup_addons():
+ """Setup and load addons/plugins."""
+ if url := udB.get_key("ADDONS_URL"):
+ subprocess.run(f"git clone -q {url} addons", shell=True)
+
+ if not os.path.exists("addons/__init__.py"):
+ with open("addons/__init__.py", "w") as f:
+ f.write("from plugins import *")
+
+ if udB.get_key("INCLUDE_ALL"):
+ # Query official plugins and sync them
+ try:
+ # Get authentication data
+ from ..state_config import temp_config_store
+ user_data = temp_config_store.get("X-TG-USER")
+ if not user_data:
+ LOGS.error("No authentication data found. Please authenticate first.")
+ return
+
+ user_id = str(user_data["id"])
+ encoded_init_data = temp_config_store.get(f"X-TG-INIT-DATA-{user_id}")
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{user_id}")
+
+ if not encoded_init_data or not encoded_hash:
+ LOGS.error("Missing authentication tokens. Please authenticate first.")
+ return
+
+ # Decode authentication data
+ init_data = base64.b64decode(encoded_init_data.encode()).decode()
+ hash_value = base64.b64decode(encoded_hash.encode()).decode()
+
+ async with aiohttp.ClientSession() as session:
+ headers = {
+ "Content-Type": "application/json",
+ "X-Telegram-Init-Data": init_data,
+ "X-Telegram-Hash": hash_value
+ }
+
+ # Get all official plugins with offset-based pagination
+ api_url = f"{CENTRAL_REPO_URL}/api/v1/plugins/official"
+ limit = 100
+ offset = 0
+ all_plugins = []
+ while True:
+ params = {
+ "limit": limit,
+ "offset": offset,
+ "sort_by": "updated_at",
+ "sort_order": "desc"
+ }
+ async with session.get(api_url, params=params, headers=headers) as response:
+ if response.status == 200:
+ data = await response.json()
+ plugins = data.get("plugins", [])
+ all_plugins.extend(plugins)
+ pagination = data.get("pagination", {})
+ if not pagination.get("has_next_page"):
+ break
+ offset += limit
+ else:
+ error_text = await response.text()
+ LOGS.error(f"Failed to fetch official plugins. Status: {response.status}, Error: {error_text}")
+ break
+
+ if not all_plugins:
+ LOGS.warning("No official plugins found")
+ return
+
+ # Create addons directory if it doesn't exist
+ addons_dir = Path(__file__).parent.parent.parent / "addons"
+ addons_dir.mkdir(exist_ok=True)
+
+ # Track plugin states for compute_diff
+ plugin_states = {}
+
+ # Async function to download a single plugin
+ async def download_plugin(plugin):
+ try:
+ plugin_id = str(plugin["id"])
+ plugin_title = plugin["title"]
+ download_url = plugin["download_link"]
+ updated_at = plugin["updated_at"]
+ file_path = plugin.get("file_path")
+
+ if not file_path:
+ LOGS.warning(f"Missing file_path for plugin {plugin_title}, skipping")
+ return None
+
+ # Generate safe filename from file_path
+ safe_filename = os.path.basename(file_path)
+ if not safe_filename.endswith('.py'):
+ safe_filename += '.py'
+
+ target_path = addons_dir / safe_filename
+
+ # Download the plugin
+ async with session.get(download_url) as plugin_response:
+ if plugin_response.status == 200:
+ plugin_content = await plugin_response.text()
+ # Write file with explicit UTF-8 encoding
+ target_path.write_text(plugin_content, encoding='utf-8')
+ # No per-plugin success log
+ return (plugin_id, updated_at)
+ else:
+ LOGS.error(f"Failed to download plugin {plugin_title}. Status: {plugin_response.status}")
+ return None
+ except Exception as e:
+ LOGS.error(f"Error processing plugin {plugin.get('title', 'unknown')}: {str(e)}")
+ return None
+
+ # Download plugins in batches using asyncio.gather
+ batch_size = 10
+ plugin_results = []
+ for i in range(0, len(all_plugins), batch_size):
+ batch = all_plugins[i:i+batch_size]
+ results = await asyncio.gather(*(download_plugin(plugin) for plugin in batch))
+ plugin_results.extend(results)
+
+ # Store plugin states in temp config
+ for result in plugin_results:
+ if result:
+ plugin_id, updated_at = result
+ plugin_states[plugin_id] = updated_at
+
+ if plugin_states:
+ LOGS.info(f"Successfully downloaded {len(plugin_states)} official plugins")
+ temp_config_store.set("OFFICIAL_PLUGINS_STATE", json.dumps(plugin_states))
+ LOGS.info(f"Successfully synced {len(plugin_states)} official plugins")
+ else:
+ LOGS.warning("No plugins were successfully processed")
+ except Exception as e:
+ LOGS.error(f"Error syncing official plugins: {str(e)}")
+
+ # Fallback to UltroidAddons if official plugins sync fails
if not os.path.exists("addons"):
- subprocess.run(
- f"git clone -q -b {Repo().active_branch} https://github.com/TeamUltroid/UltroidAddons.git addons",
- shell=True,
- )
+ try:
+ repo = Repo()
+ branch = repo.active_branch.name
+ subprocess.run(
+ f"git clone -q -b {branch} https://github.com/TeamUltroid/UltroidAddons.git addons",
+ shell=True,
+ check=True
+ )
+ except Exception as e:
+ LOGS.error(f"Failed to clone UltroidAddons: {str(e)}")
+ # Try master branch as fallback
+ subprocess.run(
+ "git clone -q https://github.com/TeamUltroid/UltroidAddons.git addons",
+ shell=True
+ )
else:
subprocess.run("cd addons && git pull -q && cd ..", shell=True)
- if not os.path.exists("addons"):
- subprocess.run(
- "git clone -q https://github.com/TeamUltroid/UltroidAddons.git addons",
- shell=True,
- )
- if os.path.exists("addons/addons.txt"):
- # generally addons req already there so it won't take much time
- # subprocess.run(
- # "rm -rf /usr/local/lib/python3.*/site-packages/pip/_vendor/.wh*"
- # )
- subprocess.run(
- f"{sys.executable} -m pip install --no-cache-dir -q -r ./addons/addons.txt",
- shell=True,
- )
-
_exclude = udB.get_key("EXCLUDE_ADDONS")
_exclude = _exclude.split() if _exclude else []
_in_only = udB.get_key("INCLUDE_ADDONS")
@@ -106,35 +365,3 @@ def load_other_plugins(addons=None, pmbot=None, manager=None, vcbot=None):
after_load=_after_load,
load_all=True,
)
-
- if not USER_MODE:
- # group manager
- if manager:
- Loader(path="assistant/manager", key="Group Manager").load()
-
- # chat via assistant
- if pmbot:
- Loader(path="assistant/pmbot.py").load(log=False)
-
- # vc bot
- if vcbot and (vcClient and not vcClient.me.bot):
- try:
- import pytgcalls # ignore: pylint
-
- if os.path.exists("vcbot"):
- if os.path.exists("vcbot/.git"):
- subprocess.run("cd vcbot && git pull", shell=True)
- else:
- rmtree("vcbot")
- if not os.path.exists("vcbot"):
- subprocess.run(
- "git clone https://github.com/TeamUltroid/VcBot vcbot", shell=True
- )
- try:
- if not os.path.exists("vcbot/downloads"):
- os.mkdir("vcbot/downloads")
- Loader(path="vcbot", key="VCBot").load(after_load=_after_load)
- except FileNotFoundError as e:
- LOGS.error(f"{e} Skipping VCBot Installation.")
- except ModuleNotFoundError:
- LOGS.error("'pytgcalls' not installed!\nSkipping loading of VCBOT.")
diff --git a/pyUltroid/startup/utils.py b/pyUltroid/startup/utils.py
index 5445dbf534..f3efda40ee 100644
--- a/pyUltroid/startup/utils.py
+++ b/pyUltroid/startup/utils.py
@@ -29,6 +29,7 @@
def load_addons(plugin_name):
+ plugin_name = str(plugin_name)
base_name = plugin_name.split("/")[-1].split("\\")[-1].replace(".py", "")
if base_name.startswith("__"):
return
diff --git a/pyUltroid/state_config.py b/pyUltroid/state_config.py
new file mode 100644
index 0000000000..d281724807
--- /dev/null
+++ b/pyUltroid/state_config.py
@@ -0,0 +1,38 @@
+import json, os
+from typing import Optional
+
+
+class TempConfigHandler:
+ path = ".config/ultroid.json"
+
+ def set(self, key: str, value: str):
+ os.makedirs(os.path.dirname(self.path), exist_ok=True)
+ try:
+ with open(self.path, "r+") as f:
+ try:
+ data = json.load(f)
+ except json.JSONDecodeError:
+ data = {}
+ except FileNotFoundError:
+ data = {}
+
+ with open(self.path, "w") as f:
+ data[key] = value
+ json.dump(data, f, indent=4)
+
+ def get(self, key: str) -> Optional[str]:
+ try:
+ with open(self.path, "r") as f:
+ data = json.load(f)
+ return data.get(key)
+ except (FileNotFoundError, json.JSONDecodeError):
+ return None
+
+ def remove(self):
+ try:
+ os.remove(self.path)
+ except FileNotFoundError:
+ pass
+
+
+temp_config_store = TempConfigHandler()
diff --git a/pyUltroid/web/cache.py b/pyUltroid/web/cache.py
new file mode 100644
index 0000000000..f065efec7b
--- /dev/null
+++ b/pyUltroid/web/cache.py
@@ -0,0 +1,47 @@
+# 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
+# .
+
+import time
+from typing import Dict, Any, Optional
+import logging
+from threading import Lock
+
+logger = logging.getLogger(__name__)
+
+
+class TTLCache:
+ def __init__(self, ttl_seconds: int = 300):
+ self._cache: Dict[str, Dict[str, Any]] = {}
+ self._lock = Lock()
+ self.ttl = ttl_seconds
+
+ def get(self, key: str) -> Optional[Dict[str, Any]]:
+ with self._lock:
+ if key not in self._cache:
+ return None
+ entry = self._cache[key]
+ if time.time() > entry["expires_at"]:
+ del self._cache[key]
+ return None
+
+ return entry["data"]
+
+ def set(self, key: str, value: Dict[str, Any]) -> None:
+ with self._lock:
+ self._cache[key] = {"data": value, "expires_at": time.time() + self.ttl}
+
+ def invalidate(self, key: str) -> None:
+ with self._lock:
+ self._cache.pop(key, None)
+
+ def clear(self) -> None:
+ with self._lock:
+ self._cache.clear()
+
+
+# Global cache instance with 5 minute TTL
+owner_cache = TTLCache(ttl_seconds=300)
diff --git a/pyUltroid/web/decorators.py b/pyUltroid/web/decorators.py
new file mode 100644
index 0000000000..1a45fc9a7c
--- /dev/null
+++ b/pyUltroid/web/decorators.py
@@ -0,0 +1,117 @@
+# 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
+# .
+
+from functools import wraps
+from typing import Callable, Optional, Any
+import json
+import os
+from aiohttp import web
+import logging
+from .. import ultroid_bot
+
+logger = logging.getLogger(__name__)
+
+
+def is_owner(user_id: Optional[int]) -> bool:
+ """Check if the user is the bot owner by comparing with ultroid_bot.me.id."""
+ try:
+ return user_id == ultroid_bot.me.id
+ except Exception as e:
+ logger.error(f"Failed to check owner status: {e}")
+ return False
+
+
+def route(
+ path: str,
+ method: str = "GET",
+ *,
+ authenticated: bool = True,
+ owner_only: bool = False,
+ description: Optional[str] = None,
+) -> Callable:
+ """
+ Route decorator with authentication and owner-only options.
+
+ Args:
+ path: URL path for the route
+ method: HTTP method (GET, POST, etc.)
+ authenticated: Whether route requires TMA authentication
+ owner_only: Whether route is restricted to bot owner (checks against ultroid_bot.me.id)
+ description: Route description for documentation
+
+ Example:
+ @route("/api/test", method="POST", authenticated=True, owner_only=True)
+ async def handler(request):
+ return web.json_response({"status": "ok"})
+ """
+
+ def decorator(handler: Callable) -> Callable:
+ @wraps(handler)
+ async def wrapped(request: web.Request) -> web.Response:
+ try:
+ # Skip auth checks for unauthenticated routes
+ if not authenticated:
+ return await handler(request)
+
+ # Get user from request (set by telegram_auth_middleware)
+ user = request.get("user", {})
+
+ # Check owner access if required
+ if owner_only:
+ user_id = user.get("id")
+ if not user_id or not is_owner(int(user_id)):
+ raise web.HTTPForbidden(
+ text=json.dumps(
+ {"error": "This endpoint is restricted to bot owner"}
+ ),
+ content_type="application/json",
+ )
+
+ return await handler(request)
+
+ except web.HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error in route handler: {str(e)}", exc_info=True)
+ return web.json_response({"error": "Internal server error"}, status=500)
+
+ # Store route metadata
+ wrapped._route = {
+ "path": path,
+ "method": method.upper(),
+ "authenticated": authenticated,
+ "owner_only": owner_only,
+ "description": description,
+ }
+
+ return wrapped
+
+ return decorator
+
+
+def setup_routes(app: web.Application, handlers: list[Callable]) -> None:
+ """
+ Setup routes from a list of handler functions decorated with @route.
+
+ Args:
+ app: aiohttp Application instance
+ handlers: List of handler functions with route decorators
+ """
+ for handler in handlers:
+ if hasattr(handler, "_route"):
+ route_info = handler._route
+ method = route_info["method"]
+ path = route_info["path"]
+
+ # Add route to app
+ app.router.add_route(method, path, handler)
+
+ logger.debug(
+ f"Added route: {method} {path} "
+ f"(auth: {route_info['authenticated']}, "
+ f"owner: {route_info['owner_only']})"
+ )
diff --git a/pyUltroid/web/middleware.py b/pyUltroid/web/middleware.py
new file mode 100644
index 0000000000..b38f8acbd3
--- /dev/null
+++ b/pyUltroid/web/middleware.py
@@ -0,0 +1,160 @@
+# 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
+# .
+
+import logging
+from aiohttp import web
+from typing import Callable, Awaitable
+import os, hmac, json, time, hashlib
+from urllib.parse import parse_qs, unquote
+from .. import udB
+
+logger = logging.getLogger(__name__)
+
+# API paths that don't require authentication
+PUBLIC_PATHS = [
+ "/health",
+ "/metrics",
+ "/api/user",
+ "/api/v1/plugins", # GET plugins list
+ "/api/v1/plugins/compute_diff", # POST compute updates
+ "/api/plugins/installed", # GET installed plugins list
+]
+
+# Paths that only allow GET without authentication
+GET_ONLY_PUBLIC_PATHS = [
+ "/api/settings/miniapp", # Only GET is public, POST requires auth
+]
+
+# Paths that start with these prefixes don't require auth
+PUBLIC_PATH_PREFIXES = [
+ "/api/v1/plugins/uploader/", # GET plugins by uploader
+]
+
+
+def parse_init_data(init_data_raw: str) -> dict:
+ try:
+ parsed = parse_qs(init_data_raw)
+ result = {}
+ for key, value in parsed.items():
+ if key == "user" and value:
+ result[key] = json.loads(unquote(value[0]))
+ elif value:
+ result[key] = value[0]
+ return result
+ except Exception as e:
+ logger.error(f"Failed to parse init data: {e}", exc_info=True)
+ return {}
+
+
+def checkValidateInitData(
+ hash_str: str, init_data_raw: str, token: str, c_str: str = "WebAppData"
+) -> bool:
+ if not all([hash_str, init_data_raw, token]):
+ logger.error("Missing required validation arguments")
+ return False
+ try:
+ data_pairs = []
+ for chunk in unquote(init_data_raw).split("&"):
+ if not chunk.startswith("hash="):
+ kv = chunk.split("=", 1)
+ if len(kv) == 2:
+ data_pairs.append(kv)
+ data_pairs.sort(key=lambda x: x[0])
+ data_check_string = "\n".join([f"{rec[0]}={rec[1]}" for rec in data_pairs])
+ logger.debug(f"Data check string for HMAC:\n{data_check_string}")
+
+ secret_key = hmac.new(c_str.encode(), token.encode(), hashlib.sha256).digest()
+ calculated_hash = hmac.new(
+ secret_key, data_check_string.encode(), hashlib.sha256
+ ).hexdigest()
+
+ logger.debug(f"Expected hash: {hash_str}")
+ logger.debug(f"Calculated hash: {calculated_hash}")
+ return calculated_hash == hash_str
+ except Exception as e:
+ logger.error(f"Error in validation: {e}", exc_info=True)
+ return False
+
+
+@web.middleware
+async def telegram_auth_middleware(
+ request: web.Request, handler: Callable[[web.Request], Awaitable[web.Response]]
+) -> web.Response:
+ # Always allow OPTIONS requests for CORS
+ # Allow public paths without authentication
+ # Allow GET requests for GET_ONLY_PUBLIC_PATHS
+ # Allow non-API paths without authentication
+ if (
+ request.method == "OPTIONS"
+ or request.path in PUBLIC_PATHS
+ or (request.path in GET_ONLY_PUBLIC_PATHS and request.method == "GET")
+ or any(request.path.startswith(prefix) for prefix in PUBLIC_PATH_PREFIXES)
+ or request.path.startswith("/api/v1/plugins/")
+ and request.method == "GET" # Allow GET for individual plugins
+ or (not request.path.startswith("/api/"))
+ ):
+ return await handler(request)
+
+ try:
+ bot_token = udB.get_key("BOT_TOKEN")
+ if not bot_token:
+ logger.error("BOT_TOKEN not set for: %s", request.path)
+ raise web.HTTPInternalServerError(
+ text=json.dumps({"error": "Server configuration error"})
+ )
+
+ auth_header = request.headers.get("Authorization", "")
+ if not auth_header.startswith("tma "):
+ logger.warning("Invalid auth header for: %s", request.path)
+ raise web.HTTPUnauthorized(
+ text=json.dumps({"error": "Authorization required"})
+ )
+
+ init_data_raw = auth_header[4:]
+ parsed_data = parse_init_data(init_data_raw)
+
+ if not parsed_data or not (hash_to_verify := parsed_data.get("hash")):
+ logger.error("Invalid/missing init data for: %s", request.path)
+ raise web.HTTPBadRequest(text=json.dumps({"error": "Invalid init data"}))
+
+ if not checkValidateInitData(
+ hash_str=hash_to_verify, init_data_raw=init_data_raw, token=bot_token
+ ):
+ logger.warning("Signature validation failed: %s", request.path)
+ raise web.HTTPUnauthorized(text=json.dumps({"error": "Invalid signature"}))
+
+ if auth_date_str := parsed_data.get("auth_date"):
+ try:
+ auth_date = int(auth_date_str)
+ if time.time() - auth_date > 3600:
+ raise web.HTTPUnauthorized(
+ text=json.dumps({"error": "Authentication expired"})
+ )
+ except ValueError:
+ raise web.HTTPBadRequest(
+ text=json.dumps({"error": "Invalid auth_date"})
+ )
+
+ for key in ["user", "start_param", "auth_date", "chat_type", "chat_instance"]:
+ request[key] = parsed_data.get(key, {} if key == "user" else None)
+
+ logger.info(
+ "Auth success: %s, user: %s",
+ request.path,
+ parsed_data.get("user", {}).get("id", "unknown"),
+ )
+ return await handler(request)
+
+ except web.HTTPException as e:
+ if e.status_code not in [400, 401, 403]:
+ logger.error("HTTP error in auth: %s", e, exc_info=True)
+ raise
+ except Exception as e:
+ logger.error("Unexpected auth error: %s", e, exc_info=True)
+ raise web.HTTPInternalServerError(
+ text=json.dumps({"error": "Internal server error"})
+ )
diff --git a/pyUltroid/web/routers/admin.py b/pyUltroid/web/routers/admin.py
new file mode 100644
index 0000000000..2bfcd2249a
--- /dev/null
+++ b/pyUltroid/web/routers/admin.py
@@ -0,0 +1,175 @@
+# 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
+# .
+
+from aiohttp import web
+import os
+import sys
+import time
+import logging
+import asyncio
+from typing import Optional
+from pathlib import Path
+from git import Repo
+import json
+
+from ..decorators import route, setup_routes
+from ..middleware import telegram_auth_middleware
+from ... import ultroid_bot
+
+logger = logging.getLogger(__name__)
+
+try:
+ from git import Repo
+except ImportError:
+ logger.error("admin: 'gitpython' module not found!")
+ Repo = None
+
+
+def is_owner(user_id: Optional[int]) -> bool:
+ """Check if the user is the bot owner."""
+ try:
+ return user_id == ultroid_bot.me.id
+ except Exception as e:
+ logger.error(f"Failed to check owner status: {e}")
+ return False
+
+
+async def check_owner(request: web.Request) -> bool:
+ """Middleware to check if the user is the bot owner."""
+ user = request.get("user", {})
+ user_id = user.get("id")
+ if not user_id or not is_owner(int(user_id)):
+ raise web.HTTPForbidden(
+ text=json.dumps({"error": "Only bot owner can access this endpoint"}),
+ content_type="application/json",
+ )
+ return True
+
+
+async def restart_bot() -> None:
+ """Restart the bot process."""
+ if os.getenv("DYNO"): # Heroku
+ os.system("kill 1")
+ else:
+ if len(sys.argv) > 1:
+ os.execl(sys.executable, sys.executable, "main.py")
+ else:
+ os.execl(sys.executable, sys.executable, "-m", "pyUltroid")
+
+
+async def update_bot(fast: bool = False) -> dict:
+ """Update the bot from the repository."""
+ try:
+ repo = Repo()
+ branch = repo.active_branch
+
+ if fast:
+ stdout, stderr, code = await bash(
+ "git pull -f && pip3 install -r requirements.txt"
+ )
+ if code != 0:
+ raise Exception(f"Fast update failed: {stderr}")
+ return {
+ "status": "success",
+ "message": "Fast update completed",
+ "restart_required": True,
+ }
+
+ # Check for updates
+ origin = repo.remotes.origin
+ origin.fetch()
+ if not repo.is_dirty():
+ commits_behind = sum(
+ 1 for _ in repo.iter_commits(f"{branch.name}..origin/{branch.name}")
+ )
+ if commits_behind == 0:
+ return {
+ "status": "success",
+ "message": "Already up to date!",
+ "update_available": False,
+ }
+
+ # Pull changes
+ stdout, stderr, code = await bash(
+ "git pull && pip3 install -r requirements.txt"
+ )
+ if code != 0:
+ raise Exception(f"Update failed: {stderr}")
+
+ return {
+ "status": "success",
+ "message": "Update successful",
+ "branch": branch.name,
+ "update_available": True,
+ "restart_required": True,
+ }
+ except Exception as e:
+ logger.error(f"Update failed: {str(e)}", exc_info=True)
+ return {"status": "error", "message": f"Update failed: {str(e)}"}
+
+
+async def bash(cmd: str) -> tuple[str, str, int]:
+ """Execute a bash command and return stdout, stderr, and return code."""
+ process = await asyncio.create_subprocess_shell(
+ cmd,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+ stdout, stderr = await process.communicate()
+ return stdout.decode(), stderr.decode(), process.returncode
+
+
+# Route handlers
+@route(
+ "/api/admin/update",
+ method="POST",
+ owner_only=True,
+ description="Update the bot from repository",
+)
+async def handle_update(request: web.Request) -> web.Response:
+ """Handle bot update request.
+ Query params:
+ fast: bool - Whether to perform a fast update (force pull)
+ """
+ await check_owner(request)
+
+ try:
+ data = await request.json() if request.can_read_body else {}
+ fast = data.get("fast", False)
+ result = await update_bot(fast)
+ return web.json_response(result)
+ except json.JSONDecodeError:
+ # If no body provided, assume default options
+ result = await update_bot(False)
+ return web.json_response(result)
+
+
+@route(
+ "/api/admin/restart", method="POST", owner_only=True, description="Restart the bot"
+)
+async def handle_restart(request: web.Request) -> web.Response:
+ """Handle bot restart request."""
+ await check_owner(request)
+
+ try:
+ # Schedule the restart
+ asyncio.create_task(restart_bot())
+ return web.json_response({"status": "success", "message": "Restart initiated"})
+ except Exception as e:
+ logger.error(f"Restart failed: {str(e)}", exc_info=True)
+ return web.json_response(
+ {"status": "error", "message": f"Restart failed: {str(e)}"}, status=500
+ )
+
+
+# List of all handlers
+handlers = [handle_update, handle_restart]
+
+
+def setup_admin_routes(app: web.Application) -> None:
+ """Setup admin routes with authentication middleware."""
+ setup_routes(app, handlers)
diff --git a/pyUltroid/web/routers/miniapp.py b/pyUltroid/web/routers/miniapp.py
new file mode 100644
index 0000000000..a07a7837b8
--- /dev/null
+++ b/pyUltroid/web/routers/miniapp.py
@@ -0,0 +1,111 @@
+# 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
+# .
+
+import logging
+from aiohttp import web
+from telethon import events
+from telethon.tl.functions.messages import SetBotPrecheckoutResultsRequest
+from telethon.tl.functions.payments import ExportInvoiceRequest
+from telethon.tl.types import (
+ DataJSON,
+ InputInvoiceMessage,
+ InputMediaInvoice,
+ LabeledPrice,
+ Invoice,
+ UpdateBotPrecheckoutQuery,
+)
+
+from ... import asst, udB
+from ..decorators import route, setup_routes
+from .admin import check_owner, is_owner
+
+logger = logging.getLogger(__name__)
+
+
+@asst.on(events.Raw(types=UpdateBotPrecheckoutQuery))
+async def handle_precheckout_query(event: UpdateBotPrecheckoutQuery):
+ """Handle pre-checkout queries to confirm transactions."""
+ logger.info(f"Received pre-checkout query: {event}")
+ try:
+ await asst(
+ SetBotPrecheckoutResultsRequest(
+ query_id=event.query_id,
+ success=True,
+ )
+ )
+ logger.info(f"Successfully answered pre-checkout query: {event.query_id}")
+ except Exception as e:
+ logger.error(
+ f"Failed to answer pre-checkout query {event.query_id}: {e}", exc_info=True
+ )
+ # Deny the transaction on failure
+ await asst(
+ SetBotPrecheckoutResultsRequest(
+ query_id=event.query_id,
+ success=False,
+ error="An internal error occurred. Please try again later.",
+ )
+ )
+
+
+@route(
+ "/api/miniapp/create_invoice",
+ method="POST",
+ description="Create a donation invoice",
+)
+async def handle_create_invoice(request: web.Request) -> web.Response:
+
+ try:
+ data = await request.json()
+ amount = int(data.get("amount"))
+
+ if not amount:
+ return web.json_response({"error": "amount is required"}, status=400)
+
+ # For XTR (Stars), no provider token is needed.
+ # The provider is Telegram.
+ title = f"Donate {amount} Stars"
+ description = f"Support Ultroid by donating {amount} Telegram Stars. ✨"
+ payload = f"ultroid_stars_{amount}".encode()
+
+ # Create the invoice media
+ invoice_media = InputMediaInvoice(
+ title=title,
+ description=description,
+ invoice=Invoice(
+ currency="XTR",
+ prices=[LabeledPrice(label=f"{amount} Star(s)", amount=amount)],
+ test=False, # Set to False for real transactions
+ phone_requested=False,
+ email_requested=False,
+ shipping_address_requested=False,
+ flexible=False,
+ ),
+ payload=payload,
+ provider="telegram",
+ provider_data=DataJSON(data="{}"),
+ start_param="ultroid-donation",
+ )
+
+ # Export the invoice link
+ exported_invoice = await asst(ExportInvoiceRequest(invoice_media=invoice_media))
+
+ return web.json_response({"url": exported_invoice.url})
+
+ except Exception as e:
+ logger.error(f"Failed to create invoice: {e}", exc_info=True)
+ return web.json_response(
+ {"error": f"Failed to create invoice: {str(e)}"}, status=500
+ )
+
+
+handlers = [handle_create_invoice]
+
+
+def setup_miniapp_routes(app: web.Application) -> None:
+ """Setup miniapp routes."""
+ setup_routes(app, handlers)
diff --git a/pyUltroid/web/routers/plugins.py b/pyUltroid/web/routers/plugins.py
new file mode 100644
index 0000000000..e21295fe2d
--- /dev/null
+++ b/pyUltroid/web/routers/plugins.py
@@ -0,0 +1,268 @@
+import logging
+import aiohttp
+import base64
+from aiohttp import web
+from ...state_config import temp_config_store
+from ...configs import CENTRAL_REPO_URL
+
+logger = logging.getLogger(__name__)
+
+
+async def get_auth_headers(request: web.Request):
+ """Get authentication headers for central API requests"""
+ from ... import ultroid_bot
+
+ # Use bot ID for auth
+ user_id = str(ultroid_bot.me.id)
+
+ # Get stored central API auth data from temp config
+ encoded_init_data = temp_config_store.get(f"X-TG-INIT-DATA-{user_id}")
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{user_id}")
+
+ if not encoded_init_data or not encoded_hash:
+ raise web.HTTPUnauthorized(text="Central API authentication data not found.")
+
+ # Decode the stored data for central API
+ init_data = base64.b64decode(encoded_init_data.encode()).decode()
+ hash_value = base64.b64decode(encoded_hash.encode()).decode()
+
+ # Return headers
+ return {
+ "Content-Type": "application/json",
+ "x-telegram-init-data": init_data,
+ "x-telegram-hash": hash_value,
+ }
+
+
+async def proxy_request(
+ request: web.Request, path: str, method: str = "GET", require_auth: bool = False
+):
+ """Generic proxy function to forward requests to central API"""
+ try:
+ # Get authentication headers if required
+ if require_auth:
+ headers = await get_auth_headers(request)
+ else:
+ headers = {"Content-Type": "application/json"}
+
+ # Prepare the target URL
+ target_url = f"{CENTRAL_REPO_URL}{path}"
+
+ async with aiohttp.ClientSession() as session:
+ # Forward the request with appropriate method
+ if method == "GET":
+ async with session.get(target_url, headers=headers) as response:
+ resp_json = await response.json()
+ return web.json_response(resp_json, status=response.status)
+
+ elif method == "POST":
+ # Handle multipart form data for file uploads
+ if request.content_type.startswith("multipart/"):
+ data = await request.post()
+ form_data = aiohttp.FormData()
+
+ # Add auth data to form fields if this is a protected route
+ if require_auth:
+ try:
+ from ... import ultroid_bot
+
+ user_id = str(ultroid_bot.me.id)
+ encoded_init_data = temp_config_store.get(
+ f"X-TG-INIT-DATA-{user_id}"
+ )
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{user_id}")
+
+ if encoded_init_data and encoded_hash:
+ init_data = base64.b64decode(
+ encoded_init_data.encode()
+ ).decode()
+ hash_value = base64.b64decode(
+ encoded_hash.encode()
+ ).decode()
+
+ # Add auth data as form fields
+ form_data.add_field("x_telegram_init_data", init_data)
+ form_data.add_field("x_telegram_hash", hash_value)
+ except Exception as e:
+ logger.error(f"Error adding auth to form: {str(e)}")
+
+ # Add the regular form data fields
+ for key, value in data.items():
+ if hasattr(value, "file"):
+ form_data.add_field(
+ key,
+ value.file,
+ filename=value.filename,
+ content_type=value.content_type,
+ )
+ else:
+ form_data.add_field(key, value)
+
+ # For multipart, we need to remove content-type from headers
+ headers_without_content_type = headers.copy()
+ if "Content-Type" in headers_without_content_type:
+ headers_without_content_type.pop("Content-Type")
+
+ async with session.post(
+ target_url, headers=headers_without_content_type, data=form_data
+ ) as response:
+ try:
+ resp_json = await response.json()
+ return web.json_response(resp_json, status=response.status)
+ except:
+ # Return raw text if not JSON
+ resp_text = await response.text()
+ return web.Response(text=resp_text, status=response.status)
+ else:
+ body = await request.json()
+ async with session.post(
+ target_url, headers=headers, json=body
+ ) as response:
+ resp_json = await response.json()
+ return web.json_response(resp_json, status=response.status)
+
+ elif method == "PUT":
+ if request.content_type.startswith("multipart/"):
+ data = await request.post()
+ form_data = aiohttp.FormData()
+
+ # Add auth data to form fields if this is a protected route
+ if require_auth:
+ try:
+ from ... import ultroid_bot
+
+ user_id = str(ultroid_bot.me.id)
+ encoded_init_data = temp_config_store.get(
+ f"X-TG-INIT-DATA-{user_id}"
+ )
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{user_id}")
+
+ if encoded_init_data and encoded_hash:
+ init_data = base64.b64decode(
+ encoded_init_data.encode()
+ ).decode()
+ hash_value = base64.b64decode(
+ encoded_hash.encode()
+ ).decode()
+
+ # Add auth data as form fields
+ form_data.add_field("x_telegram_init_data", init_data)
+ form_data.add_field("x_telegram_hash", hash_value)
+ except Exception as e:
+ logger.error(f"Error adding auth to form for PUT: {str(e)}")
+
+ # Add the regular form data fields
+ for key, value in data.items():
+ if hasattr(value, "file"):
+ form_data.add_field(
+ key,
+ value.file,
+ filename=value.filename,
+ content_type=value.content_type,
+ )
+ else:
+ form_data.add_field(key, value)
+
+ # For multipart, remove Content-Type from headers
+ headers_without_content_type = headers.copy()
+ if "Content-Type" in headers_without_content_type:
+ headers_without_content_type.pop("Content-Type")
+
+ async with session.put(
+ target_url, headers=headers_without_content_type, data=form_data
+ ) as response:
+ try:
+ resp_json = await response.json()
+ return web.json_response(resp_json, status=response.status)
+ except:
+ # Return raw text if not JSON
+ resp_text = await response.text()
+ return web.Response(text=resp_text, status=response.status)
+ else:
+ body = await request.json()
+ async with session.put(
+ target_url, headers=headers, json=body
+ ) as response:
+ resp_json = await response.json()
+ return web.json_response(resp_json, status=response.status)
+
+ elif method == "DELETE":
+ async with session.delete(target_url, headers=headers) as response:
+ resp_json = await response.json()
+ return web.json_response(resp_json, status=response.status)
+
+ except web.HTTPUnauthorized as e:
+ return web.json_response({"error": str(e)}, status=401)
+ except Exception as e:
+ logger.error(f"Error in proxy request: {e}", exc_info=True)
+ return web.json_response(
+ {"error": f"Internal server error: {str(e)}"}, status=500
+ )
+
+
+# Plugin handlers
+async def proxy_plugins_list(request: web.Request):
+ """Proxy GET /api/v1/plugins"""
+ return await proxy_request(request, "/api/v1/plugins", require_auth=True)
+
+
+async def proxy_plugin_get(request: web.Request):
+ """Proxy GET /api/v1/plugins/{plugin_id}"""
+ plugin_id = request.match_info["plugin_id"]
+ return await proxy_request(
+ request, f"/api/v1/plugins/{plugin_id}", require_auth=True
+ )
+
+
+async def proxy_plugin_upload(request: web.Request):
+ """Proxy POST /api/v1/plugins"""
+ return await proxy_request(request, "/api/v1/plugins", "POST", require_auth=True)
+
+
+async def proxy_plugin_update(request: web.Request):
+ """Proxy PUT /api/v1/plugins/{plugin_id}"""
+ plugin_id = request.match_info["plugin_id"]
+ return await proxy_request(
+ request, f"/api/v1/plugins/{plugin_id}", "PUT", require_auth=True
+ )
+
+
+async def proxy_plugin_delete(request: web.Request):
+ """Proxy DELETE /api/v1/plugins/{plugin_id}"""
+ plugin_id = request.match_info["plugin_id"]
+ return await proxy_request(
+ request, f"/api/v1/plugins/{plugin_id}", "DELETE", require_auth=True
+ )
+
+
+async def proxy_plugins_by_uploader(request: web.Request):
+ """Proxy GET /api/v1/plugins/uploader/{uploader_id}"""
+ uploader_id = request.match_info["uploader_id"]
+ return await proxy_request(
+ request, f"/api/v1/plugins/uploader/{uploader_id}", require_auth=True
+ )
+
+
+async def proxy_plugins_compute_diff(request: web.Request):
+ """Proxy POST /api/v1/plugins/compute_diff"""
+ return await proxy_request(
+ request, "/api/v1/plugins/compute_diff", "POST", require_auth=True
+ )
+
+
+def setup_plugin_routes(app):
+ """Setup routes for plugins API"""
+ # Public routes - no auth required
+ app.router.add_get("/api/v1/plugins", proxy_plugins_list)
+ app.router.add_get(
+ "/api/v1/plugins/uploader/{uploader_id}", proxy_plugins_by_uploader
+ )
+ app.router.add_get("/api/v1/plugins/{plugin_id}", proxy_plugin_get)
+
+ # Protected routes - auth required
+ app.router.add_post("/api/v1/plugins", proxy_plugin_upload)
+ app.router.add_post("/api/v1/plugins/compute_diff", proxy_plugins_compute_diff)
+ app.router.add_put("/api/v1/plugins/{plugin_id}", proxy_plugin_update)
+ app.router.add_delete("/api/v1/plugins/{plugin_id}", proxy_plugin_delete)
+
+ logger.info("Plugin proxy routes configured at /api/v1/plugins")
diff --git a/pyUltroid/web/server.py b/pyUltroid/web/server.py
new file mode 100644
index 0000000000..3c0373efba
--- /dev/null
+++ b/pyUltroid/web/server.py
@@ -0,0 +1,515 @@
+# 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
+# .
+
+from aiohttp import web
+import json
+from typing import Dict, Optional
+import logging
+import os
+from .. import ultroid_bot, udB
+from pyUltroid.fns.helper import time_formatter
+from telethon.utils import get_display_name
+import time
+import ssl
+from pathlib import Path
+from .tg_scraper import scraper
+from .middleware import telegram_auth_middleware
+import aiohttp_cors
+from .routers.admin import setup_admin_routes
+from .routers.plugins import setup_plugin_routes
+from .routers.miniapp import setup_miniapp_routes
+from .cache import owner_cache
+from ..configs import Var
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# Track server start time
+start_time = time.time()
+
+
+class UltroidWebServer:
+ def __init__(self):
+ # Check for BOT_TOKEN
+ bot_token = os.getenv("BOT_TOKEN")
+ if not bot_token:
+ logger.error(
+ "BOT_TOKEN environment variable is not set! Authentication will fail."
+ )
+ else:
+ logger.info("BOT_TOKEN is properly configured.")
+
+ # Important: telegram_auth_middleware must come before no_cors_middleware
+ self.app = web.Application(middlewares=[telegram_auth_middleware])
+ self.setup_routes()
+ self.port = Var.PORT
+ self.bot = ultroid_bot
+ self.ssl_context = None
+
+ def setup_routes(self):
+ """Setup basic API routes"""
+ # Add routes
+ self.app.router.add_get("/api/user", self.get_ultroid_owner_info)
+ self.app.router.add_get("/health", self.health_check)
+ self.app.router.add_post("/api/settings/miniapp", self.save_miniapp_settings)
+ self.app.router.add_get("/api/settings/miniapp", self.get_miniapp_settings)
+
+ # Setup admin, plugin, and miniapp routes
+ setup_admin_routes(self.app)
+ logger.info("Admin routes configured at /api/admin/")
+
+ setup_plugin_routes(self.app)
+ logger.info("Plugin routes configured at /api/v1/plugins/")
+
+ setup_miniapp_routes(self.app)
+ logger.info("MiniApp routes configured at /api/miniapp/")
+
+ # Add plugin installation routes
+ self.app.router.add_post("/api/plugins/install", self.install_plugin)
+ self.app.router.add_get("/api/plugins/installed", self.get_installed_plugins)
+ logger.info("Plugin installation routes configured at /api/plugins/")
+
+ cors = aiohttp_cors.setup(
+ self.app,
+ defaults={
+ "*": aiohttp_cors.ResourceOptions(
+ allow_credentials=True,
+ expose_headers="*",
+ allow_headers="*",
+ allow_methods="*",
+ ) },
+ ) # Add CORS to all registered routes
+ for route in list(self.app.router.routes()):
+ cors.add(route)
+
+ async def health_check(self, request: web.Request) -> web.Response:
+ """Health check endpoint that doesn't require auth"""
+ return web.json_response({"status": "ok"})
+
+ async def save_miniapp_settings(self, request: web.Request) -> web.Response:
+ """Save mini app and bot configuration settings to udB"""
+ try:
+ data = await request.json()
+
+ # Handle both single key-value and multiple settings
+ settings_to_save = []
+
+ if "key" in data and "value" in data:
+ # Single key-value pair
+ settings_to_save.append({"key": data["key"], "value": data["value"]})
+ elif "settings" in data:
+ # Multiple settings
+ settings_to_save = data["settings"]
+ else:
+ return web.json_response({"error": "Missing key/value or settings parameter"}, status=400)
+
+ if not settings_to_save:
+ return web.json_response({"error": "No settings provided"}, status=400)
+
+ # Bot configuration settings that should be stored directly in udB
+ bot_config_keys = [
+ "DUAL_MODE", "BOT_MODE", "HNDLR", "DUAL_HNDLR",
+ "SUDO", "SUDO_HNDLR", "ADDONS", "PLUGIN_CHANNEL", "EMOJI_IN_HELP",
+ "PMSETTING", "INLINE_PM", "PM_TEXT", "PMPIC", "PMWARNS", "AUTOAPPROVE",
+ "PMLOG", "PMLOGGROUP", "PMBOT", "STARTMSG", "STARTMEDIA", "BOT_INFO_START",
+ "ALIVE_TEXT", "ALIVE_PIC", "INLINE_PIC", "TAG_LOG", "FBAN_GROUP_ID",
+ "EXCLUDE_FED", "RMBG_API", "DEEP_AI", "OCR_API", "GDRIVE_FOLDER_ID",
+ "VC_SESSION", "PMBOT_FSUB"
+ ]
+
+ # Get current miniapp settings once
+ miniapp_settings = udB.get_key("MINIAPP_SETTINGS") or {}
+ miniapp_settings_updated = False
+
+ # Process all settings
+ for setting in settings_to_save:
+ key = setting.get("key")
+ value = setting.get("value")
+
+ if not key:
+ continue
+
+ if key in bot_config_keys:
+ # Handle special data type conversions based on callbackstuffs.py
+ if key == "PMWARNS":
+ # PMWARNS should be stored as integer
+ try:
+ value = int(value)
+ except (ValueError, TypeError):
+ return web.json_response(
+ {"error": f"PMWARNS must be a valid integer, got: {value}"}, status=400
+ )
+ elif key in ["DUAL_MODE", "BOT_MODE", "SUDO", "ADDONS", "PMSETTING",
+ "INLINE_PM", "AUTOAPPROVE", "PMLOG", "PMBOT"]:
+ # Boolean settings that should be stored as string "True"/"False"
+ if isinstance(value, bool):
+ value = "True" if value else "False"
+ elif value not in ["True", "False", True, False]:
+ # Convert other truthy/falsy values
+ value = "True" if value else "False"
+ elif key == "PMBOT_FSUB" and isinstance(value, (list, tuple)):
+ # PMBOT_FSUB should be stored as string representation of list
+ value = str(value)
+ elif key == "EXCLUDE_FED" and isinstance(value, str):
+ # EXCLUDE_FED can be space-separated IDs, keep as string
+ pass
+
+ # Store bot configuration directly in udB
+ udB.set_key(key, value)
+ logger.info(f"Saved Bot config setting: {key}={value}")
+ else:
+ # Store mini app settings in MINIAPP_SETTINGS
+ miniapp_settings[key] = value
+ miniapp_settings_updated = True
+ logger.info(f"Saved Mini App setting: {key}={value}")
+
+ # Save miniapp settings once if any were updated
+ if miniapp_settings_updated:
+ udB.set_key("MINIAPP_SETTINGS", miniapp_settings)
+
+ saved_count = len(settings_to_save)
+ return web.json_response(
+ {"success": True, "message": f"{saved_count} setting(s) saved successfully"}
+ )
+ except Exception as e:
+ logger.error(f"Error saving settings: {str(e)}", exc_info=True)
+ return web.json_response(
+ {"error": f"Failed to save settings: {str(e)}"}, status=500
+ )
+
+ async def get_miniapp_settings(self, request: web.Request) -> web.Response:
+ """Get mini app and bot configuration settings from udB"""
+ try:
+ # Get mini app settings
+ miniapp_settings = udB.get_key("MINIAPP_SETTINGS") or {}
+
+ # Get bot configuration settings
+ bot_config_keys = [
+ "DUAL_MODE", "BOT_MODE", "HNDLR", "DUAL_HNDLR",
+ "SUDO", "SUDO_HNDLR", "ADDONS", "PLUGIN_CHANNEL", "EMOJI_IN_HELP",
+ "PMSETTING", "INLINE_PM", "PM_TEXT", "PMPIC", "PMWARNS", "AUTOAPPROVE",
+ "PMLOG", "PMLOGGROUP", "PMBOT", "STARTMSG", "STARTMEDIA", "BOT_INFO_START",
+ "ALIVE_TEXT", "ALIVE_PIC", "INLINE_PIC", "TAG_LOG", "FBAN_GROUP_ID",
+ "EXCLUDE_FED", "RMBG_API", "DEEP_AI", "OCR_API", "GDRIVE_FOLDER_ID",
+ "VC_SESSION", "PMBOT_FSUB"
+ ]
+ bot_settings = {}
+
+ for key in bot_config_keys:
+ value = udB.get_key(key)
+ if value is not None:
+ bot_settings[key] = value
+
+ # Merge both settings (only include keys that have values)
+ all_settings = {**miniapp_settings, **bot_settings}
+
+ return web.json_response(all_settings)
+ except Exception as e:
+ logger.error(f"Error getting settings: {str(e)}", exc_info=True)
+ return web.json_response(
+ {"error": f"Failed to get settings: {str(e)}"}, status=500
+ )
+
+ async def install_plugin(self, request: web.Request) -> web.Response:
+ """Install a plugin by ID and store it in udB INSTALLED_PLUGINS"""
+ try:
+ # Check if user is authenticated
+ if not request.get('user'):
+ return web.json_response(
+ {"error": "Authentication required"}, status=401
+ )
+
+ data = await request.json()
+ plugin_id = data.get("plugin_id")
+
+ if not plugin_id:
+ return web.json_response(
+ {"error": "Missing plugin_id parameter"}, status=400
+ )
+
+ # Import necessary modules for plugin installation
+ import aiohttp
+ import base64
+ from pathlib import Path
+ from ..configs import CENTRAL_REPO_URL
+ from ..state_config import temp_config_store
+
+ # Get authentication data
+ user_data = temp_config_store.get("X-TG-USER")
+ if not user_data:
+ return web.json_response(
+ {"error": "No authentication data found"}, status=401
+ )
+
+ user_id = str(user_data["id"])
+ encoded_init_data = temp_config_store.get(f"X-TG-INIT-DATA-{user_id}")
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{user_id}")
+
+ if not encoded_init_data or not encoded_hash:
+ return web.json_response(
+ {"error": "Missing authentication tokens"}, status=401
+ )
+
+ # Decode authentication data
+ init_data = base64.b64decode(encoded_init_data.encode()).decode()
+ hash_value = base64.b64decode(encoded_hash.encode()).decode()
+
+ async with aiohttp.ClientSession() as session:
+ # First, get plugin details
+ api_url = f"{CENTRAL_REPO_URL}/api/v1/plugins/{plugin_id}"
+ headers = {
+ "Content-Type": "application/json",
+ "X-Telegram-Init-Data": init_data,
+ "X-Telegram-Hash": hash_value
+ }
+
+ async with session.get(api_url, headers=headers) as response:
+ if response.status != 200:
+ error_text = await response.text()
+ return web.json_response(
+ {"error": f"Failed to fetch plugin details: {error_text}"},
+ status=response.status
+ )
+
+ plugin_data = await response.json()
+
+ # Download the plugin file
+ download_url = plugin_data.get("download_link")
+ if not download_url:
+ return web.json_response(
+ {"error": "Plugin download link not available"}, status=400
+ )
+
+ async with session.get(download_url) as download_response:
+ if download_response.status != 200:
+ return web.json_response(
+ {"error": "Failed to download plugin file"},
+ status=download_response.status
+ )
+
+ plugin_content = await download_response.text()
+
+ # Create addons directory if it doesn't exist
+ addons_dir = Path("addons")
+ addons_dir.mkdir(exist_ok=True)
+
+ # Generate safe filename
+ plugin_title = plugin_data.get("title", "plugin")
+ file_path = plugin_data.get("file_path", "")
+ if file_path:
+ safe_filename = Path(file_path).name
+ else:
+ safe_filename = f"{plugin_title.lower().replace(' ', '_')}_{plugin_id}.py"
+
+ if not safe_filename.endswith('.py'):
+ safe_filename += '.py'
+
+ target_path = addons_dir / safe_filename
+
+ # Write the plugin file
+ target_path.write_text(plugin_content, encoding='utf-8')
+
+ # Update installed plugins list in udB
+ installed_plugins = udB.get_key("INSTALLED_PLUGINS") or []
+ if str(plugin_id) not in installed_plugins:
+ installed_plugins.append(str(plugin_id))
+ udB.set_key("INSTALLED_PLUGINS", installed_plugins)
+
+ # Load the plugin dynamically
+ try:
+ from ..loader import Loader
+ from ..startup.utils import load_addons
+ from ..startup.loader import _after_load
+
+ # Load the specific plugin
+ loader = Loader(path="addons", key="Addons")
+ # Load only the newly installed plugin
+ loader.load_single_plugin(
+ target_path,
+ func=load_addons,
+ after_load=_after_load
+ )
+
+ logger.info(f"Successfully loaded plugin: {plugin_title}")
+ except Exception as load_error:
+ logger.error(f"Error loading plugin {plugin_title}: {str(load_error)}")
+
+ return web.json_response({
+ "success": True,
+ "message": f"Plugin '{plugin_title}' installed successfully",
+ "plugin_id": plugin_id,
+ "filename": safe_filename
+ })
+
+ except Exception as e:
+ logger.error(f"Error installing plugin: {str(e)}", exc_info=True)
+ return web.json_response(
+ {"error": f"Failed to install plugin: {str(e)}"}, status=500
+ )
+
+ async def get_installed_plugins(self, request: web.Request) -> web.Response:
+ """Get list of installed plugin IDs including official plugins from INCLUDE_ALL"""
+ try:
+ # Get manually installed plugins
+ installed_plugins = udB.get_key("INSTALLED_PLUGINS") or []
+
+ # Get official plugins installed via INCLUDE_ALL
+ from ..state_config import temp_config_store
+ official_plugins_state = temp_config_store.get("OFFICIAL_PLUGINS_STATE")
+
+ if official_plugins_state:
+ try:
+ import json
+ official_plugins = json.loads(official_plugins_state)
+ # Add official plugin IDs to installed list
+ for plugin_id in official_plugins.keys():
+ if plugin_id not in installed_plugins:
+ installed_plugins.append(plugin_id)
+ except Exception as e:
+ logger.error(f"Error parsing official plugins state: {str(e)}")
+
+ return web.json_response({
+ "installed_plugins": installed_plugins
+ })
+ except Exception as e:
+ logger.error(f"Error getting installed plugins: {str(e)}", exc_info=True)
+ return web.json_response(
+ {"error": f"Failed to get installed plugins: {str(e)}"}, status=500
+ )
+
+ async def get_ultroid_owner_info(self, request: web.Request) -> web.Response:
+ cache_key = f"owner_info_{self.bot.me.id}"
+ cached_data = owner_cache.get(cache_key)
+
+ if cached_data:
+ logger.debug("Returning cached owner info")
+ return web.json_response(cached_data)
+
+ try:
+ stats = {
+ "uptime": time_formatter(time.time() - start_time),
+ }
+
+ public_data = {
+ "name": get_display_name(self.bot.me),
+ "bio": "",
+ "avatar": "",
+ "username": self.bot.me.username,
+ "telegram_url": (
+ f"https://t.me/{self.bot.me.username}"
+ if self.bot.me.username
+ else None
+ ),
+ "stats": stats,
+ "skills": ["Telegram Bot Management", "Automation", "Python"],
+ "user_id": self.bot.me.id,
+ "authenticated_user": request.get("user", {}),
+ "start_param": request.get("start_param"),
+ "auth_date": request.get("auth_date"),
+ }
+
+ if self.bot.me.username:
+ try:
+ profile_info = await scraper.get_profile_info(self.bot.me.username)
+ if profile_info:
+ if "bio" in profile_info and profile_info["bio"]:
+ public_data["bio"] = profile_info["bio"]
+ if "avatar" in profile_info and profile_info["avatar"]:
+ public_data["avatar"] = profile_info["avatar"]
+ except Exception as e:
+ logger.error(f"Error fetching profile info: {e}")
+
+ owner_cache.set(cache_key, public_data)
+ return web.json_response(public_data)
+
+ except Exception as e:
+ logger.error(f"Error in get_ultroid_owner_info: {e}", exc_info=True)
+ return web.json_response(
+ {"error": "Failed to fetch owner info"}, status=500
+ )
+
+ async def _setup_web_app_build(self):
+ """Setup web app build directory if it exists"""
+ from pyUltroid.scripts.webapp import fetch_recent_release
+
+ # Download and extract the latest webapp release
+ success = await fetch_recent_release()
+
+ # First try using the downloaded webapp from resources
+ webapp_path = Path("resources/webapp")
+ if success and webapp_path.exists():
+ logger.info(f"Setting up webapp at {webapp_path.absolute()}")
+ if Var.MINIAPP_URL:
+ with open(webapp_path / "config.json", "w") as f:
+ config_data = {
+ "apiUrl": Var.MINIAPP_URL,
+ }
+ json.dump(config_data, f, indent=4)
+
+ # Add specific handler for root path to serve index.html
+ index_file = webapp_path / "index.html"
+ if index_file.exists():
+
+ async def root_handler(request):
+ logger.debug("Serving index.html for root path /")
+ return web.FileResponse(index_file)
+
+ # Add root handler first to ensure it has priority
+ self.app.router.add_get("/", root_handler)
+ logger.info(f"Added specific route for / to serve {index_file}")
+
+ try:
+ self.app.router.add_static("/", path=webapp_path)
+ logger.info(f"Serving static files from {webapp_path}")
+ except Exception as e:
+ logger.error(f"Failed to add static route: {e}", exc_info=True)
+ return
+
+ async def handle_index(request):
+ index_file = webapp_path / "index.html"
+ if index_file.exists():
+ logger.debug(f"Serving index.html for route: {request.path}")
+ return web.FileResponse(index_file)
+ else:
+ logger.warning(f"index.html not found at {index_file}")
+ return web.Response(
+ text="Web application is being setup...",
+ content_type="text/html",
+ )
+
+ # Add fallback route for SPA navigation
+ self.app.router.add_get("/{tail:.*}", handle_index)
+ logger.info("Added SPA fallback handler")
+ else:
+ logger.info("Web app build is disabled!")
+
+ async def cleanup(self):
+ """Clean up resources when server shuts down"""
+ await scraper.close()
+ owner_cache.clear()
+
+ async def start(self, host: str = "0.0.0.0", port: Optional[int] = None):
+ """Asynchronously starts the web server."""
+ if Var.RENDER_WEB:
+ logger.info("Setting up web app build...")
+ await self._setup_web_app_build()
+
+ self.app.on_shutdown.append(self.cleanup)
+
+ runner = web.AppRunner(self.app)
+ await runner.setup()
+
+ _port = port or self.port
+ site = web.TCPSite(runner, host, _port, ssl_context=self.ssl_context)
+ await site.start()
+
+ logger.info(
+ f"Starting {'HTTPS' if self.ssl_context else 'HTTP'} server on {host}:{_port}"
+ )
+
+
+ultroid_server = UltroidWebServer()
diff --git a/pyUltroid/web/tg_scraper.py b/pyUltroid/web/tg_scraper.py
new file mode 100644
index 0000000000..fea234e828
--- /dev/null
+++ b/pyUltroid/web/tg_scraper.py
@@ -0,0 +1,115 @@
+# 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
+# .
+
+import aiohttp
+import logging
+import re
+from bs4 import BeautifulSoup
+from typing import Dict, Optional, Tuple
+
+logger = logging.getLogger(__name__)
+
+
+class TelegramProfileScraper:
+ """Utility to scrape public information from Telegram web profiles"""
+
+ def __init__(self):
+ self.session = None
+ self.cache = {} # Simple cache to avoid repeated requests
+
+ async def _get_session(self):
+ if self.session is None or self.session.closed:
+ self.session = aiohttp.ClientSession(
+ headers={
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+ }
+ )
+ return self.session
+
+ async def close(self):
+ if self.session and not self.session.closed:
+ await self.session.close()
+
+ async def get_profile_info(self, username: str) -> Optional[Dict]:
+ """
+ Scrape profile information from a public Telegram profile
+
+ Args:
+ username: Telegram username without the @ symbol
+
+ Returns:
+ Dictionary with profile information or None if not found
+ """
+ # Check cache first
+ if username in self.cache:
+ return self.cache[username]
+
+ try:
+ # Clean the username
+ username = username.strip().lower()
+ if username.startswith("@"):
+ username = username[1:]
+
+ url = f"https://t.me/{username}"
+ session = await self._get_session()
+
+ async with session.get(url) as response:
+ if response.status != 200:
+ logger.warning(
+ f"Failed to fetch profile for {username}: HTTP {response.status}"
+ )
+ return None
+
+ html = await response.text()
+
+ # Parse the HTML
+ soup = BeautifulSoup(html, "html.parser")
+
+ # Extract profile information
+ result = {}
+
+ # Get profile image
+ img_tag = soup.select_one("img.tgme_page_photo_image")
+ if img_tag and "src" in img_tag.attrs:
+ result["avatar"] = img_tag["src"]
+
+ # Get bio
+ bio_div = soup.select_one("div.tgme_page_description")
+ if bio_div:
+ result["bio"] = bio_div.get_text(strip=True)
+
+ # Get name
+ title_tag = soup.select_one("div.tgme_page_title")
+ if title_tag:
+ result["name"] = title_tag.get_text(strip=True)
+
+ # Cache the result
+ self.cache[username] = result
+ return result
+
+ except Exception as e:
+ logger.error(f"Error scraping profile for {username}: {e}")
+ return None
+
+ async def get_profile_image(self, username: str) -> Optional[str]:
+ """Get just the profile image URL"""
+ profile = await self.get_profile_info(username)
+ return profile.get("avatar") if profile else None
+
+ async def get_profile_bio(self, username: str) -> Optional[str]:
+ """Get just the profile bio"""
+ profile = await self.get_profile_info(username)
+ return profile.get("bio") if profile else None
+
+
+# Singleton instance
+scraper = TelegramProfileScraper()
+
+if __name__ == "__main__":
+ import asyncio
+
+ asyncio.run(scraper.get_profile_info("karboncopy"))
diff --git a/requirements.txt b/requirements.txt
index acbc790d4b..46a0a262ae 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,12 +1,21 @@
# Important Requirements here.
telethon
-gitpython
https://github.com/New-dev0/Telethon-Patch/archive/main.zip
python-decouple
python-dotenv
telegraph
enhancer
requests
+gitpython
+aiohttp
+aiohttp-cors
+catbox-uploader
+cloudscraper
+pynacl
+telegraph
+enhancer
+requests
aiohttp
catbox-uploader
-cloudscraper
\ No newline at end of file
+cloudscraper
+redis
diff --git a/strings/strings/en.yml b/strings/strings/en.yml
index 85dd02758f..ce8c6b0a7e 100644
--- a/strings/strings/en.yml
+++ b/strings/strings/en.yml
@@ -601,7 +601,7 @@ help_chats: " -\n\n• `{i}delchat `\n Delete the grou
help_cleanaction: " -\n\n•`{i}addclean`\n Clean all Upcoming action msg in added chat like someone joined/left/pin etc.\n\n•`{i}remclean`\n Remove chat from database.\n\n•`{i}listclean`\n To get list of all chats where its activated.\n\n"
help_converter: " -\n\n• `{i}convert `\n Reply to media to convert it into gif / image / webm / normal sticker.\n\n• `{i}doc `\n Reply to a text msg to save it in a file.\n\n• `{i}open`\n Reply to a file to reveal it's text.\n\n• `{i}rename `\n Rename the file\n\n• `{i}thumbnail `\n Upload Your file with your custom thumbnail.\n"
help_core: " -\n\n• `{i}install `\n To install the plugin,\n `{i}install f`\n To force Install.\n\n• `{i}uninstall `\n To unload and remove the plugin.\n\n• `{i}load `\n To load unloaded unofficial plugin.\n\n• `{i}unload `\n To unload unofficial plugin.\n\n• `{i}help `\n Shows you a help menu (like this) for every plugin.\n\n• `{i}getaddons `\n Load Plugins from the given raw link.\n"
-help_database: " -\n\n• **DataBase Commands, do not use if you don't know what it is.**\n\n• `{i}setdb key | value`\n Set Value in Database.\n e.g :\n `{i}setdb hi there`\n `{i}setdb hi there | ultroid here`\n `{i}setdb --extend variable value` or `{i}setdb -e variable value` to add the value to the exiting values in db.\n\n• `{i}deldb key`\n Delete Key from DB.\n\n• `{i}rendb old keyname | new keyname`\n Update Key Name\n"
+help_database: " -\n\n• **DataBase Commands, do not use if you don't know what it is.**\n\n• `{i}setdb key | value`\n Set Value in Database.\n e.g :\n `{i}setdb hi there`\n `{i}setdb hi there | ultroid here`\n `{i}setdb --extend variable value` or `{i}setdb -e variable value` to add the value to the exiting values in db.\n\n• `{i}deldb key`\n Delete Key from DB.\n\n• `{i}rendb old keyname | new keyname`\n Update Key Name\n\n• `{i}get var `\n Get value of the given variable name.\n\n• `{i}get type `\n Get variable type.\n\n• `{i}get db `\n Get db value of the given key.\n\n• `{i}get keys`\n Get all redis keys.\n"
help_devtools: " -\n\n• `{i}bash `\n• `{i}bash -c ` Carbon image as command output.\n Run linux commands on telegram.\n\n• `{i}eval `\n Evaluate python commands on telegram.\n Shortcuts:\n client = bot = event.client\n e = event\n p = print\n reply = await event.get_reply_message()\n chat = event.chat_id\n\n• `{i}cpp `\n Run c++ code from Telegram.\n\n• `{i}sysinfo`\n Shows System Info.\n"
help_downloadupload: " -\n\n• `{i}ul `\n Upload files on telegram.\n Use following arguments before or after filename as per requirement:\n `--stream` to upload as stream.\n `--delete` to delete file after uploading.\n `--no-thumb` to upload without thumbnail.\n\n• `{i}dl `\n Reply to file to download.\n\n• `{i}download (| filename)`\n Download using DDL. Will autogenerate filename if not given.\n"
help_echo: "\n\n•`{i}addecho `\n Start Auto Echo message of Replied user.\n\n•`{i}remecho `\n Turn It off\n\n•`{i}listecho `\n To Get list.\n"
@@ -640,7 +640,6 @@ help_tools: " -\n\n• `{i}circle`\n Reply to a audio song or gif to get vide
help_unsplash: " -\n\n• {i}unsplash ; \n Unsplash Image Search.\n"
help_usage: "\n\n• `{i}usage`\n Get overall usage.\n\n• `{i}usage heroku`\n Get heroku stats.\n\n• `{i}usage db`\n Get database storage usage.\n"
help_utilities: " -\n\n• `{i}kickme` : Leaves the group.\n\n• `{i}date` : Show Calender.\n\n• `{i}listreserved`\n List all usernames (channels/groups) you own.\n\n• `{i}stats` : See your profile stats.\n\n• `{i}paste` - `Include long text / Reply to text file.`\n\n• `{i}info `\n Reply to someone's msg.\n\n• `{i}invite `\n Add user to the chat.\n\n• `{i}rmbg `\n Remove background from that picture.\n\n• `{i}telegraph `\n Upload media/text to telegraph.\n\n• `{i}json `\n Get the json encoding of the message.\n\n• `{i}suggest or `\n Create a Yes/No poll for the replied suggestion.\n\n• `{i}ipinfo ` : Get info about that IP address.\n\n• `{i}cpy `\n Copy the replied message, with formatting. Expires in 24hrs.\n• `{i}pst`\n Paste the copied message, with formatting.\n\n• `{i}thumb ` : Download the thumbnail of the replied file.\n\n• `{i}getmsg `\n Get messages from chats with forward/copy restrictions.\n"
-help_variables: " -\n\n• `{i}get var `\n Get value of the given variable name.\n\n• `{i}get type `\n Get variable type.\n\n• `{i}get db `\n Get db value of the given key.\n\n• `{i}get keys`\n Get all redis keys.\n"
help_vctools: " -\n\n• `{i}startvc`\n Start Group Call in a group.\n\n• `{i}stopvc`\n Stop Group Call in a group.\n\n• `{i}vctitle `\n Change the title Group call.\n\n• `{i}vcinvite`\n Invite all members of group in Group Call.\n (You must be joined)\n"
help_videotools: " -\n\n•`{i}sample `\n Creates Short sample of video..\n\n• `{i}vshots `\n Creates screenshot of video..\n\n• `{i}vtrim - in seconds`\n Crop a Lengthy video..\n"
help_warn: "\n\n•`{i}warn `\n Gives Warn.\n\n•`{i}resetwarn `\n To reset All Warns.\n\n•`{i}warns `\n To Get List of Warnings of a user.\n\n•`{i}setwarn | `\n Set Number in warn count for warnings\n After putting ' | ' mark put action like ban/mute/kick\n Its Default 3 kick\n Example : `setwarn 5 | mute`\n\n"
diff --git a/upload.py b/upload.py
new file mode 100644
index 0000000000..ea9a48b491
--- /dev/null
+++ b/upload.py
@@ -0,0 +1,338 @@
+#!/usr/bin/env python3
+# Script to upload all plugins from a folder to the Ultroid Plugin Store.
+
+import asyncio
+import aiohttp
+import json
+import base64
+import os
+
+from dotenv import load_dotenv
+load_dotenv()
+
+import logging
+from pathlib import Path
+from typing import Optional
+
+# AI-related imports
+try:
+ from openai import OpenAI
+except ImportError:
+ print("Error: 'openai' is not installed. Please install it with 'pip install openai'")
+ exit(1)
+
+
+# Import from Ultroid modules
+from pyUltroid.state_config import temp_config_store
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+def get_plugin_metadata_with_ai(file_content: str, model_client: OpenAI) -> Optional[dict]:
+ """
+ Analyzes plugin code using an AI model to extract metadata.
+
+ Args:
+ file_content: The source code of the plugin.
+ model_client: The AI client to use for analysis.
+
+ Returns:
+ A dictionary with extracted metadata or None on failure.
+ """
+ logger.info("Analyzing plugin with AI to extract metadata...")
+ system_prompt = """
+You are an expert Python developer specializing in Ultroid plugins.
+Your task is to analyze a given plugin file and extract specific metadata.
+Respond with ONLY a valid JSON object containing the following keys:
+- "description": A short, clear description of what the plugin does, derived from its docstring or overall purpose.
+- "commands": A list of strings, where each string is a command pattern (e.g., "ping", "start"). Find these in `@ultroid_cmd(pattern="...")` decorators.
+- "packages": A list of strings, where each string is an external pip package required by the plugin. Analyze the import statements to determine these. Common built-in modules should be ignored.
+"""
+
+ user_prompt = f"""
+Analyze this Ultroid plugin code and return the metadata as a JSON object.
+
+```python
+{file_content}
+```
+"""
+
+ try:
+ response = model_client.chat.completions.create(
+ messages=[
+ {
+ "role": "system",
+ "content": system_prompt
+ },
+ {
+ "role": "user",
+ "content": user_prompt
+ }
+ ],
+ model="meta-llama/llama-4-scout-17b-16e-instruct",
+ response_format={"type": "json_object"},
+ )
+
+ content = response.choices[0].message.content
+ if content.startswith("```json"):
+ content = content[7:]
+ if content.endswith("```"):
+ content = content[:-3]
+ print(content)
+ metadata = json.loads(content)
+ logger.info("Successfully extracted metadata from AI.")
+ return metadata
+
+ except (json.JSONDecodeError, IndexError) as e:
+ logger.error(f"Failed to get or parse AI response: {e}")
+ return None
+ except Exception as e:
+ logger.error(f"An unexpected error occurred during AI analysis: {e}")
+ return None
+
+async def upload_addon_plugins():
+ """
+ Scans the UltroidAddons folder, extracts metadata for each plugin,
+ and uploads them to the plugin store.
+ """
+ api_url = "http://localhost:8055"
+ bot_id = 1444249738 # Placeholder, adjust if needed
+
+ # Get stored authentication data
+ encoded_init_data = temp_config_store.get(f"X-TG-INIT-DATA-{bot_id}")
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{bot_id}")
+
+ if not encoded_init_data or not encoded_hash:
+ logger.error("Authentication data not found. Please authenticate with Ultroid Central first.")
+ return
+
+ init_data = base64.b64decode(encoded_init_data.encode()).decode()
+ hash_value = base64.b64decode(encoded_hash.encode()).decode()
+
+ # Initialize AI Client
+ token = os.environ.get("GROQ_API_KEY")
+ if not token:
+ logger.error("GROQ_API_KEY not found in environment variables.")
+ return
+
+ client = OpenAI(
+ base_url="https://api.groq.com/openai/v1",
+ api_key=token,
+ )
+
+ # Path to addons, assuming it's a sibling to the 'Ultroid' directory
+ addon_path = Path("addons_copy")
+ if not addon_path.exists() or not addon_path.is_dir():
+ logger.error(f"Could not find the UltroidAddons directory at: {addon_path}")
+ return
+
+ logger.info(f"Scanning for plugins in: {addon_path}")
+
+ async with aiohttp.ClientSession() as session:
+ for plugin_path in sorted(addon_path.glob("*.py")):
+ if plugin_path.name == "__init__.py":
+ continue
+
+ logger.info(f"--- Processing plugin: {plugin_path.name} ---")
+
+ try:
+ content = plugin_path.read_text(encoding='utf-8')
+ metadata = get_plugin_metadata_with_ai(content, client)
+
+ if not metadata:
+ logger.warning(f"Could not get metadata for {plugin_path.name}. Skipping.")
+ continue
+
+ # Prepare JSON data
+ json_data = {
+ "title": plugin_path.stem.replace('_', ' ').title(),
+ "description": metadata.get("description", "N/A"),
+ "tags": ["community", plugin_path.stem],
+ "packages": metadata.get("packages", []),
+ "commands": metadata.get("commands", []),
+ "is_trusted": True,
+ "is_official": True,
+ "plugin_filename": plugin_path.name,
+ "plugin_content": base64.b64encode(content.encode('utf-8')).decode('utf-8')
+ }
+
+ # Prepare headers with authentication
+ headers = {
+ "Content-Type": "application/json",
+ "X-Telegram-Init-Data": init_data,
+ "X-Telegram-Hash": hash_value
+ }
+
+ # Upload the plugin
+ logger.info(f"Uploading '{json_data['title']}'...")
+ plugin_url = f"{api_url}/api/v1/plugins"
+
+ async with session.post(
+ plugin_url,
+ json=json_data,
+ headers=headers
+ ) as response:
+ status = response.status
+ resp_text = await response.text()
+
+ if status in (200, 201):
+ logger.info(f"Successfully uploaded '{json_data['title']}'. Status: {status}")
+ else:
+ logger.error(f"Failed to upload '{json_data['title']}'. Status: {status}, Response: {resp_text[:200]}")
+
+ except Exception as e:
+ logger.error(f"An unexpected error occurred while processing {plugin_path.name}: {e}")
+
+ # Optional: Add a small delay to avoid overwhelming the server or AI service
+ await asyncio.sleep(1)
+
+async def delete_all_plugins():
+ """Delete all plugins uploaded by the current user."""
+ api_url = "http://localhost:8055"
+ bot_id = 1444249738 # Placeholder, adjust if needed
+
+ # Get stored authentication data
+ encoded_init_data = temp_config_store.get(f"X-TG-INIT-DATA-{bot_id}")
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{bot_id}")
+
+ if not encoded_init_data or not encoded_hash:
+ logger.error("Authentication data not found. Please authenticate with Ultroid Central first.")
+ return
+
+ init_data = base64.b64decode(encoded_init_data.encode()).decode()
+ hash_value = base64.b64decode(encoded_hash.encode()).decode()
+
+ # Prepare headers with authentication
+ headers = {
+ "Content-Type": "application/json",
+ "X-Telegram-Init-Data": init_data,
+ "X-Telegram-Hash": hash_value
+ }
+
+ async with aiohttp.ClientSession() as session:
+ plugin_url = f"{api_url}/api/v1/plugins/user/plugins"
+
+ async with session.delete(
+ plugin_url,
+ headers=headers
+ ) as response:
+ status = response.status
+ resp_text = await response.text()
+
+ if status in (200, 201):
+ data = json.loads(resp_text)
+ logger.info(f"Successfully deleted {data.get('deleted_count', 0)} plugins")
+ else:
+ logger.error(f"Failed to delete plugins. Status: {status}, Response: {resp_text[:200]}")
+
+async def upload_single_plugin(plugin_path_str):
+ """
+ Upload a single plugin file to the plugin store.
+ """
+ api_url = "http://localhost:8055"
+ bot_id = 1444249738 # Placeholder, adjust if needed
+
+ # Get stored authentication data
+ encoded_init_data = temp_config_store.get(f"X-TG-INIT-DATA-{bot_id}")
+ encoded_hash = temp_config_store.get(f"X-TG-HASH-{bot_id}")
+
+ if not encoded_init_data or not encoded_hash:
+ logger.error("Authentication data not found. Please authenticate with Ultroid Central first.")
+ return
+
+ init_data = base64.b64decode(encoded_init_data.encode()).decode()
+ hash_value = base64.b64decode(encoded_hash.encode()).decode()
+
+ # Initialize AI Client
+ token = os.environ.get("GROQ_API_KEY")
+ if not token:
+ logger.error("GROQ_API_KEY not found in environment variables.")
+ return
+
+ client = OpenAI(
+ base_url="https://api.groq.com/openai/v1",
+ api_key=token,
+ )
+
+ plugin_path = Path(plugin_path_str)
+ if not plugin_path.exists() or not plugin_path.is_file():
+ logger.error(f"Plugin file not found: {plugin_path}")
+ return
+
+ logger.info(f"Processing single plugin: {plugin_path.name}")
+
+ async with aiohttp.ClientSession() as session:
+ try:
+ content = plugin_path.read_text(encoding='utf-8')
+ metadata = get_plugin_metadata_with_ai(content, client)
+
+ if not metadata:
+ logger.warning(f"Could not get metadata for {plugin_path.name}.")
+ return
+
+ # Prepare JSON data
+ json_data = {
+ "title": plugin_path.stem.replace('_', ' ').title(),
+ "description": metadata.get("description", "N/A"),
+ "tags": ["community", plugin_path.stem],
+ "packages": metadata.get("packages", []),
+ "commands": metadata.get("commands", []),
+ "is_trusted": True,
+ "is_official": True,
+ "plugin_filename": plugin_path.name,
+ "plugin_content": base64.b64encode(content.encode('utf-8')).decode('utf-8')
+ }
+
+ # Prepare headers with authentication
+ headers = {
+ "Content-Type": "application/json",
+ "X-Telegram-Init-Data": init_data,
+ "X-Telegram-Hash": hash_value
+ }
+
+ # Upload the plugin
+ logger.info(f"Uploading '{json_data['title']}'...")
+ plugin_url = f"{api_url}/api/v1/plugins"
+
+ async with session.post(
+ plugin_url,
+ json=json_data,
+ headers=headers
+ ) as response:
+ status = response.status
+ resp_text = await response.text()
+
+ if status in (200, 201):
+ logger.info(f"Successfully uploaded '{json_data['title']}'. Status: {status}")
+ else:
+ logger.error(f"Failed to upload '{json_data['title']}'. Status: {status}, Response: {resp_text[:200]}")
+
+ except Exception as e:
+ logger.error(f"An unexpected error occurred while processing {plugin_path.name}: {e}")
+
+if __name__ == "__main__":
+ import sys
+ import argparse
+
+ if "GROQ_API_KEY" not in os.environ:
+ print("Error: GROQ_API_KEY environment variable is not set.")
+ print("Please set it to a valid Groq API key.")
+ sys.exit(1)
+
+ parser = argparse.ArgumentParser(description='Ultroid Plugin Store Management')
+ parser.add_argument('action', choices=['upload', 'delete', 'upload_one'],
+ help='Action to perform: upload all, upload one, or delete all plugins')
+ parser.add_argument('--path', type=str, help='Path to the plugin file (for upload_one)')
+
+ args = parser.parse_args()
+
+ if args.action == 'upload':
+ asyncio.run(upload_addon_plugins())
+ elif args.action == 'delete':
+ asyncio.run(delete_all_plugins())
+ elif args.action == 'upload_one':
+ if not args.path:
+ print("Please provide --path to the plugin file for upload_one.")
+ sys.exit(1)
+ asyncio.run(upload_single_plugin(args.path))
\ No newline at end of file