Update README and alive command for @overspend1 fork

- Updated README title to show OVERSPEND1 FORK
- Changed maintainer credit to @overspend1
- Updated alive command to show @overspend1 as creator instead of Meliodas
This commit is contained in:
overspend1
2025-07-25 20:27:05 +02:00
commit 8fe355ed0c
68 changed files with 8981 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
config*.env
*session*
venv/
down*
__pycache__
.idea/
conf_backup/
logs/
.mypy_cache
psutil
app/temp

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
FROM python:3.12.7-slim-bookworm
ENV PIP_NO_CACHE_DIR=1 \
LANG=C.UTF-8 \
DEBIAN_FRONTEND=noninteractive
WORKDIR /app/
RUN apt -qq update && apt -qq upgrade -y && \
apt -qq install -y --no-install-recommends \
apt-utils \
build-essential coreutils \
curl \
ffmpeg \
mediainfo \
neofetch \
git \
wget && \
pip install -U pip setuptools wheel && \
git config --global user.email "98635854+thedragonsinn@users.noreply.github.com" && \
git config --global user.name "thedragonsinn"
EXPOSE 8080
CMD bash -c "$(curl -fsSL https://raw.githubusercontent.com/thedragonsinn/plain-ub/main/docker_start.sh)"

90
README.md Normal file
View File

@@ -0,0 +1,90 @@
## PLAIN UB - OVERSPEND1 FORK
![Header Image](assets/dark.png#gh-dark-mode-only)
![Header Image](assets/light.png#gh-light-mode-only)
A simple Telegram User-Bot.
> Forked and maintained by @overspend1
## Example Plugins:
<details>
<summary></summary>
* Basic Plugin:
```python
from app import BOT, bot, Message
@bot.add_cmd(cmd="test")
async def test_function(bot: BOT, message: Message):
await message.reply("Testing....")
"""Your rest of the code."""
```
* Plugin with Multiple Commands:
Instead of stacking @add_cmd you can pass in a list of command triggers.
```python
from app import BOT, bot, Message
@bot.add_cmd(cmd=["cmd1", "cmd2"])
async def test_function(bot: BOT, message: Message):
if message.cmd=="cmd1":
await message.reply("cmd1 triggered function")
"""Your rest of the code."""
```
* Plugin with DB access:
```python
from app import BOT, bot, Message, CustomDB
TEST_COLLECTION = CustomDB["TEST_COLLECTION"]
@bot.add_cmd(cmd="add_data")
async def test_function(bot: BOT, message: Message):
async for data in TEST_COLLECTION.find():
"""Your rest of the code."""
# OR
await TEST_COLLECTION.add_data(data={"_id":"test", "data":"some_data"})
await TEST_COLLECTION.delete_data(id="test")
```
* Conversational Plugin:
* Bound Method
```python
from pyrogram import filters
from app import BOT, bot, Message
@bot.add_cmd(cmd="test")
async def test_function(bot: BOT, message: Message):
response = await message.get_response(
filters=filters.text&filters.user([1234]),
timeout=10,
)
# Will return First text it receives in chat where cmd was ran
""" rest of the code """
```
* Conversational
```python
from app import BOT, bot, Message, Convo
from pyrogram import filters
@bot.add_cmd(cmd="test")
async def test_function(bot: BOT, message: Message):
async with Convo(
client=bot,
chat_id=1234,
filters=filters.text,
timeout=10
) as convo:
await convo.get_response(timeout=10)
await convo.send_message(text="abc", get_response=True, timeout=8)
# and so on
```
</details>

1
app/__init__.py Normal file
View File

@@ -0,0 +1 @@
from ub_core import BOT, LOGGER, Config, Convo, CustomDB, Message, bot

13
app/__main__.py Normal file
View File

@@ -0,0 +1,13 @@
import sys
from app import LOGGER, Config, bot
if Config.CMD_TRIGGER == Config.SUDO_TRIGGER:
LOGGER.error("CMD_TRIGGER and SUDO_TRIGGER can't be the same")
sys.exit(1)
if __name__ == "__main__":
bot.run(bot.boot())
else:
LOGGER.error("Wrong Start Command.\nUse 'python -m app'")

39
app/extra_config.py Normal file
View File

@@ -0,0 +1,39 @@
from os import getenv
from pyrogram.enums import ChatMemberStatus
ALIVE_MEDIA: str = getenv("ALIVE_MEDIA", "https://telegra.ph/file/a1d35a86c7f54a96188a9.png")
ADMIN_STATUS = {ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER}
BOT_NAME = getenv("BOT_NAME", "PLAIN-UB")
CUSTOM_PACK_NAME = getenv("CUSTOM_PACK_NAME")
DISABLED_SUPERUSERS: list[int] = []
FBAN_LOG_CHANNEL: int = int(getenv("FBAN_LOG_CHANNEL") or getenv("LOG_CHAT"))
FBAN_SUDO_ID: int = int(getenv("FBAN_SUDO_ID", 0))
FBAN_SUDO_TRIGGER: str = getenv("FBAN_SUDO_TRIGGER")
GEMINI_API_KEY: str = getenv("GEMINI_API_KEY")
LOAD_HANDLERS: bool = True
MESSAGE_LOGGER_CHAT: int = int(getenv("MESSAGE_LOGGER_CHAT") or getenv("LOG_CHAT"))
PM_GUARD: bool = False
PM_LOGGER: bool = False
PM_LOGGER_THREAD_ID: int = int(getenv("PM_LOGGER_THREAD_ID", 0)) or None
TAG_LOGGER: bool = False
TAG_LOGGER_THREAD_ID: int = int(getenv("TAG_LOGGER_THREAD_ID", 0)) or None
UPSTREAM_REPO: str = getenv("UPSTREAM_REPO", "https://github.com/thedragonsinn/plain-ub")
USE_LEGACY_KANG: int = int(getenv("USE_LEGACY_KANG", 0))

29
app/plugins/admin/ban.py Normal file
View File

@@ -0,0 +1,29 @@
from pyrogram.types import User
from app import BOT, Message
@BOT.add_cmd(cmd=["ban", "unban", "unmute"])
async def ban_or_unban(bot: BOT, message: Message) -> None:
if not message.chat._raw.admin_rights:
await message.reply("Cannot perform action without being admin.")
return
user, reason = await message.extract_user_n_reason()
if not isinstance(user, User):
await message.reply(user, del_in=10)
return
action = bot.ban_chat_member if message.cmd == "ban" else bot.unban_chat_member
if message.cmd == "unmute":
action_str = "Unmuted"
else:
action_str = f"{message.cmd.capitalize()}ned"
try:
await action(chat_id=message.chat.id, user_id=user.id) # NOQA
await message.reply(text=f"{action_str}: {user.mention}\nReason: {reason}")
except Exception as e:
await message.reply(text=e, del_in=10)

270
app/plugins/admin/fbans.py Normal file
View File

@@ -0,0 +1,270 @@
import asyncio
from pyrogram import filters
from pyrogram.enums import ChatMemberStatus, ChatType
from pyrogram.errors import UserNotParticipant
from pyrogram.types import Chat, User
from ub_core.utils.helpers import get_name
from app import BOT, Config, CustomDB, Message, bot, extra_config
FBAN_TASK_LOCK = asyncio.Lock()
FED_DB = CustomDB["FED_LIST"]
BASIC_FILTER = filters.user([609517172, 2059887769, 1376954911, 885745757]) & ~filters.service
FBAN_REGEX = BASIC_FILTER & filters.regex(
r"(New FedBan|"
r"starting a federation ban|"
r"Starting a federation ban|"
r"start a federation ban|"
r"FedBan Reason update|"
r"FedBan reason updated|"
r"Would you like to update this reason)"
)
UNFBAN_REGEX = BASIC_FILTER & filters.regex(r"(New un-FedBan|I'll give|Un-FedBan)")
@bot.add_cmd(cmd="addf")
async def add_fed(bot: BOT, message: Message):
"""
CMD: ADDF
INFO: Add a Fed Chat to DB.
USAGE:
.addf | .addf NAME
"""
data = dict(name=message.input or message.chat.title, type=str(message.chat.type))
await FED_DB.add_data({"_id": message.chat.id, **data})
text = f"#FBANS\n<b>{data['name']}</b>: <code>{message.chat.id}</code> added to FED LIST."
await message.reply(text=text, del_in=5, block=True)
await bot.log_text(text=text, type="info")
@bot.add_cmd(cmd="delf")
async def remove_fed(bot: BOT, message: Message):
"""
CMD: DELF
INFO: Delete a Fed from DB.
FLAGS: -all to delete all feds.
USAGE:
.delf | .delf id | .delf -all
"""
if "-all" in message.flags:
await FED_DB.drop()
await message.reply("FED LIST cleared.")
return
chat: int | str | Chat = message.input or message.chat
name = ""
if isinstance(chat, Chat):
name = f"Chat: {chat.title}\n"
chat = chat.id
elif chat.lstrip("-").isdigit():
chat = int(chat)
deleted: int = await FED_DB.delete_data(id=chat)
if deleted:
text = f"#FBANS\n<b>{name}</b><code>{chat}</code> removed from FED LIST."
await message.reply(text=text, del_in=8)
await bot.log_text(text=text, type="info")
else:
await message.reply(text=f"<b>{name or chat}</b> not in FED LIST.", del_in=8)
@bot.add_cmd(cmd="listf")
async def fed_list(bot: BOT, message: Message):
"""
CMD: LISTF
INFO: View Connected Feds.
FLAGS: -id to list Fed Chat IDs.
USAGE: .listf | .listf -id
"""
output: str = ""
total = 0
async for fed in FED_DB.find():
output += f'<b>• {fed["name"]}</b>\n'
if "-id" in message.flags:
output += f' <code>{fed["_id"]}</code>\n'
total += 1
if not total:
await message.reply("You don't have any Feds Connected.")
return
output: str = f"List of <b>{total}</b> Connected Feds:\n\n{output}"
await message.reply(output, del_in=30, block=True)
@bot.add_cmd(cmd=["fban", "fbanp"])
async def fed_ban(bot: BOT, message: Message):
progress: Message = await message.reply("")
extracted_info = await get_user_reason(message=message, progress=progress)
if not extracted_info:
await progress.edit("Unable to extract user info.")
return
user_id, user_mention, reason = extracted_info
if user_id in [Config.OWNER_ID, *Config.SUPERUSERS, *Config.SUDO_USERS]:
await progress.edit("Cannot Fban Owner/Sudo users.")
return
proof_str: str = ""
if message.cmd == "fbanp":
if not message.replied:
await progress.edit("Reply to a proof")
return
proof = await message.replied.forward(extra_config.FBAN_LOG_CHANNEL)
proof_str = f"\n{ {proof.link} }"
reason = f"{reason}{proof_str}"
if message.replied and message.chat.type != ChatType.PRIVATE:
try:
if message.chat._raw.admin_rights:
await message.replied.reply(
text=f"!dban {reason}", disable_preview=True, del_in=3, block=False
)
except UserNotParticipant:
pass
fban_cmd: str = f"/fban <a href='tg://user?id={user_id}'>{user_id}</a> {reason}"
await perform_fed_task(
user_id=user_id,
user_mention=user_mention,
command=fban_cmd,
task_filter=FBAN_REGEX,
task_type="Fban",
reason=reason,
progress=progress,
message=message,
)
@bot.add_cmd(cmd="unfban")
async def un_fban(bot: BOT, message: Message):
progress: Message = await message.reply("")
extracted_info = await get_user_reason(message=message, progress=progress)
if not extracted_info:
await progress.edit("Unable to extract user info.")
return
user_id, user_mention, reason = extracted_info
unfban_cmd: str = f"/unfban <a href='tg://user?id={user_id}'>{user_id}</a> {reason}"
await perform_fed_task(
user_id=user_id,
user_mention=user_mention,
command=unfban_cmd,
task_filter=UNFBAN_REGEX,
task_type="Un-FBan",
reason=reason,
progress=progress,
message=message,
)
async def get_user_reason(message: Message, progress: Message) -> tuple[int, str, str] | None:
user, reason = await message.extract_user_n_reason()
if isinstance(user, str):
await progress.edit(user)
return
if not isinstance(user, User):
user_id = user
user_mention = f"<a href='tg://user?id={user_id}'>{user_id}</a>"
else:
user_id = user.id
user_mention = user.mention
return user_id, user_mention, reason
async def perform_fed_task(*args, **kwargs):
async with FBAN_TASK_LOCK:
await _perform_fed_task(*args, **kwargs)
async def _perform_fed_task(
user_id: int,
user_mention: str,
command: str,
task_filter: filters.Filter,
task_type: str,
reason: str,
progress: Message,
message: Message,
):
await progress.edit("")
total: int = 0
failed: list[str] = []
async for fed in FED_DB.find():
chat_id = int(fed["_id"])
total += 1
try:
cmd: Message = await bot.send_message(
chat_id=chat_id, text=command, disable_preview=True
)
response: Message | None = await cmd.get_response(filters=task_filter, timeout=8)
if not response:
failed.append(fed["name"])
elif "Would you like to update this reason" in response.text:
await response.click("Update reason")
except Exception as e:
await bot.log_text(
text=f"An Error occured while banning in fed: {fed['name']} [{chat_id}]"
f"\nError: {e}",
type=task_type.upper(),
)
failed.append(fed["name"])
continue
await asyncio.sleep(1)
if not total:
await progress.edit("You Don't have any feds connected!")
return
resp_str = (
f" <b>{task_type}ned</b> {user_mention}"
f"\n<b>ID</b>: {user_id}"
f"\n<b>Reason</b>: {reason}"
f"\n<b>Initiated in</b>: {message.chat.title or 'PM'}"
)
if failed:
resp_str += f"\n<b>Failed</b> in: {len(failed)}/{total}\n" + "\n".join(failed)
else:
resp_str += f"\n<b>Status</b>: {task_type}ned in <b>{total}</b> feds."
if not message.is_from_owner:
resp_str += f"\n\n<b>By</b>: {get_name(message.from_user)}"
await bot.send_message(
chat_id=extra_config.FBAN_LOG_CHANNEL, text=resp_str, disable_preview=True
)
await progress.edit(text=resp_str, del_in=5, block=True, disable_preview=True)
await handle_sudo_fban(command=command)
async def handle_sudo_fban(command: str):
if not (extra_config.FBAN_SUDO_ID and extra_config.FBAN_SUDO_TRIGGER):
return
sudo_cmd = command.replace("/", extra_config.FBAN_SUDO_TRIGGER, 1)
await bot.send_message(chat_id=extra_config.FBAN_SUDO_ID, text=sudo_cmd, disable_preview=True)

View File

@@ -0,0 +1,77 @@
import asyncio
from datetime import UTC, datetime, timedelta
from pyrogram.types import User
from app import BOT, Message
from app.extra_config import ADMIN_STATUS
@BOT.add_cmd(cmd="kick")
async def kick_user(bot: BOT, message: Message):
user, reason = await message.extract_user_n_reason()
if not isinstance(user, User):
await message.reply(user, del_in=10)
return
try:
await bot.ban_chat_member(chat_id=message.chat.id, user_id=user.id)
await asyncio.sleep(2)
await bot.unban_chat_member(chat_id=message.chat.id, user_id=user.id)
await message.reply(text=f"{message.cmd.capitalize()}ed: {user.mention}\nReason: {reason}")
except Exception as e:
await message.reply(text=e, del_in=10)
@BOT.add_cmd(cmd="kick_im", allow_sudo=False)
async def kick_inactive_members(bot: BOT, message: Message):
"""
CMD: KICK_IM
INFO: Kick inactive members with message count less than 10
"""
if not message.chat._raw.admin_rights:
await message.reply("Cannot kick members without being admin.")
return
count = 0
chat_id = message.chat.id
async with bot.Convo(client=bot, chat_id=chat_id, from_user=message.from_user.id) as convo:
async for member in bot.get_chat_members(chat_id):
if member.status in ADMIN_STATUS:
continue
user = member.user
message_count = await bot.search_messages_count(chat_id=chat_id, from_user=user.id)
if message_count >= 10:
continue
try:
prompt = await convo.send_message(
text=f"Kick {user.mention} with total of {message_count} messages in chat?"
f"\nreply with y to continue"
)
convo.reply_to_message_id = prompt.id
confirmation = await convo.get_response()
if confirmation.text == "y":
await bot.ban_chat_member(
chat_id=chat_id,
user_id=user.id,
until_date=datetime.now(UTC) + timedelta(seconds=60),
)
await prompt.edit(f"Kicked {user.mention}")
count += 1
else:
await prompt.edit("Aborted, continuing onto next the member.")
except TimeoutError:
pass
await message.reply(f"Kicked {count} inactive members.")

35
app/plugins/admin/mute.py Normal file
View File

@@ -0,0 +1,35 @@
from pyrogram.types import ChatPermissions, User
from app import BOT, Message
@BOT.add_cmd(cmd="mute")
async def mute_or_unmute(bot: BOT, message: Message):
if not message.chat._raw.admin_rights:
await message.reply("Cannot mute members without being admin.")
return
user, reason = await message.extract_user_n_reason()
if not isinstance(user, User):
await message.reply(user, del_in=10)
return
try:
await bot.restrict_chat_member(
chat_id=message.chat.id,
user_id=user.id,
permissions=ChatPermissions(
can_send_messages=False,
can_pin_messages=False,
can_invite_users=False,
can_change_info=False,
can_send_media_messages=False,
can_send_polls=False,
can_send_other_messages=False,
can_add_web_page_previews=False,
),
)
await message.reply(text=f"{message.cmd.capitalize()}d: {user.mention}\nReason: {reason}")
except Exception as e:
await message.reply(text=e, del_in=10)

View File

@@ -0,0 +1,111 @@
import asyncio
from pyrogram.enums import ChatMembersFilter, ChatMemberStatus
from pyrogram.errors import FloodWait
from pyrogram.types import ChatPrivileges, User
from app import BOT, Message
from app.extra_config import ADMIN_STATUS
DEMOTE_PRIVILEGES = ChatPrivileges(can_manage_chat=False)
NO_PRIVILEGES = ChatPrivileges(
can_manage_chat=True,
can_manage_video_chats=False,
can_pin_messages=False,
can_delete_messages=False,
can_change_info=False,
can_restrict_members=False,
can_invite_users=False,
can_promote_members=False,
is_anonymous=False,
)
@BOT.add_cmd(cmd=["promote", "demote"])
async def promote_or_demote(bot: BOT, message: Message) -> None:
"""
CMD: PROMOTE | DEMOTE
INFO: Add/Remove an Admin.
FLAGS:
PROMOTE: -full for full rights, -anon for anon admin
USAGE:
PROMOTE: .promote [ -anon | -full ] [ UID | REPLY | @ ] Title[Optional]
DEMOTE: .demote [ UID | REPLY | @ ]
"""
response: Message = await message.reply(f"Trying to {message.cmd.capitalize()}.....")
my_status = await bot.get_chat_member(chat_id=message.chat.id, user_id=bot.me.id)
my_privileges = my_status.privileges
if not (my_status.status in ADMIN_STATUS and my_privileges.can_promote_members):
await response.edit("You don't to have enough rights to do this.")
return
user, title = await message.extract_user_n_reason()
if not isinstance(user, User):
await response.edit(user, del_in=10)
return
my_privileges.can_promote_members = "-full" in message.flags
my_privileges.is_anonymous = "-anon" in message.flags
promote = message.cmd == "promote"
if promote:
final_privileges = NO_PRIVILEGES if "-wr" in message.flags else my_privileges
else:
final_privileges = DEMOTE_PRIVILEGES
response_text = f"{message.cmd.capitalize()}d: {user.mention}"
try:
await bot.promote_chat_member(
chat_id=message.chat.id, user_id=user.id, privileges=final_privileges
)
if promote:
await asyncio.sleep(1)
await bot.set_administrator_title(
chat_id=message.chat.id, user_id=user.id, title=title or "Admin"
)
if title:
response_text += f"\nTitle: {title}"
if "-wr" in message.flags:
response_text += "\nWithout Rights: True"
await response.edit(text=response_text)
except Exception as e:
await response.edit(text=e, del_in=10, block=True)
@BOT.add_cmd(cmd="demote_all", allow_sudo=False)
async def demote_all(bot: BOT, message: Message):
me = await bot.get_chat_member(message.chat.id, bot.me.id)
if me.status != ChatMemberStatus.OWNER:
await message.reply("Cannot Demote all without being Chat Owner.")
return
resp = await message.reply("Hang on demoting all Admins...")
count = 0
async for member in bot.get_chat_members(
chat_id=message.chat.id, filter=ChatMembersFilter.ADMINISTRATORS
):
try:
await bot.promote_chat_member(
chat_id=message.chat.id, user_id=member.user.id, privileges=DEMOTE_PRIVILEGES
)
except FloodWait as f:
await asyncio.sleep(f.value + 10)
await bot.promote_chat_member(
chat_id=message.chat.id, user_id=member.user.id, privileges=DEMOTE_PRIVILEGES
)
await asyncio.sleep(0.5)
count += 1
await resp.edit(f"Demoted <b>{count}</b> admins in {message.chat.title}.")
await resp.log()

View File

@@ -0,0 +1,46 @@
import asyncio
from datetime import UTC, datetime, timedelta
from pyrogram.errors import FloodWait
from app import BOT, Message
from app.extra_config import ADMIN_STATUS
@BOT.add_cmd(cmd="zombies")
async def clean_zombies(bot: BOT, message: Message):
if not message.chat._raw.admin_rights:
await message.reply("Cannot clean zombies without being admin.")
return
zombies = 0
admin_zombies = 0
response = await message.reply("Cleaning Zombies....\nthis may take a while")
async for member in bot.get_chat_members(chat_id=message.chat.id):
try:
if member.user.is_deleted:
if member.status in ADMIN_STATUS:
admin_zombies += 1
continue
zombies += 1
await bot.ban_chat_member(
chat_id=message.chat.id,
user_id=member.user.id,
until_date=datetime.now(UTC) + timedelta(seconds=60),
)
await asyncio.sleep(1)
except FloodWait as e:
await asyncio.sleep(e.value + 3)
resp_str = f"Cleaned <b>{zombies}</b> zombies."
if admin_zombies:
resp_str += f"\n<b>{admin_zombies}</b> Admin Zombie(s) not Removed."
await response.edit(resp_str)

View File

@@ -0,0 +1,2 @@
from .client import client, async_client, Response
from .config import AIConfig, DB_SETTINGS

View File

@@ -0,0 +1,157 @@
import pickle
from io import BytesIO
from google.genai.chats import AsyncChat
from pyrogram.enums import ChatType, ParseMode
from app import BOT, Convo, Message, bot
from app.plugins.ai.gemini import AIConfig, Response, async_client
from app.plugins.ai.gemini.utils import create_prompts, run_basic_check
@bot.add_cmd(cmd="aic")
@run_basic_check
async def ai_chat(bot: BOT, message: Message):
"""
CMD: AICHAT
INFO: Have a Conversation with Gemini AI.
FLAGS:
"-s": use search
"-i": use image gen/edit mode
-a: audio output
-sp: multi speaker output
USAGE:
.aic hello
keep replying to AI responses with text | media [no need to reply in DM]
After 5 mins of Idle bot will export history and stop chat.
use .load_history to continue
"""
chat = async_client.chats.create(**AIConfig.get_kwargs(message.flags))
await do_convo(chat=chat, message=message)
@bot.add_cmd(cmd="lh")
@run_basic_check
async def history_chat(bot: BOT, message: Message):
"""
CMD: LOAD_HISTORY
INFO: Load a Conversation with Gemini AI from previous session.
USAGE:
.lh {question} [reply to history document]
"""
reply = message.replied
if not message.input:
await message.reply(f"Ask a question along with {message.trigger}{message.cmd}")
return
try:
assert reply.document.file_name == "AI_Chat_History.pkl"
except (AssertionError, AttributeError):
await message.reply("Reply to a Valid History file.")
return
resp = await message.reply("`Loading History...`")
doc = await reply.download(in_memory=True)
doc.seek(0)
pickle.load(doc)
await resp.edit("__History Loaded... Resuming chat__")
chat = async_client.chats.create(**AIConfig.get_kwargs(message.flags))
await do_convo(chat=chat, message=message)
CONVO_CACHE: dict[str, Convo] = {}
async def do_convo(chat: AsyncChat, message: Message):
chat_id = message.chat.id
old_conversation = CONVO_CACHE.get(message.unique_chat_user_id)
if old_conversation in Convo.CONVO_DICT[chat_id]:
Convo.CONVO_DICT[chat_id].remove(old_conversation)
if message.chat.type in (ChatType.PRIVATE, ChatType.BOT):
reply_to_user_id = None
else:
reply_to_user_id = message._client.me.id
conversation_object = Convo(
client=message._client,
chat_id=chat_id,
timeout=300,
check_for_duplicates=False,
from_user=message.from_user.id,
reply_to_user_id=reply_to_user_id,
)
CONVO_CACHE[message.unique_chat_user_id] = conversation_object
try:
async with conversation_object:
prompt = await create_prompts(message)
reply_to_id = message.id
while True:
ai_response = await chat.send_message(prompt)
prompt_message = await send_and_get_resp(
convo_obj=conversation_object,
response=ai_response,
reply_to_id=reply_to_id,
)
try:
prompt = await create_prompts(prompt_message, is_chat=True, check_size=False)
except Exception as e:
prompt_message = await send_and_get_resp(
conversation_object, str(e), reply_to_id=reply_to_id
)
prompt = await create_prompts(prompt_message, is_chat=True, check_size=False)
reply_to_id = prompt_message.id
except TimeoutError:
await export_history(chat, message)
finally:
CONVO_CACHE.pop(message.unique_chat_user_id, 0)
async def send_and_get_resp(
convo_obj: Convo,
response,
reply_to_id: int | None = None,
) -> Message:
response = Response(response)
if text := response.text():
await convo_obj.send_message(
text=f"**>\n•><**\n{text}",
reply_to_id=reply_to_id,
parse_mode=ParseMode.MARKDOWN,
disable_preview=True,
)
if response.image:
await convo_obj.send_photo(photo=response.image_file, reply_to_id=reply_to_id)
if response.audio:
await convo_obj.send_voice(
voice=response.audio_file,
waveform=response.audio_file.waveform,
reply_to_id=reply_to_id,
duration=response.audio_file.duration,
)
return await convo_obj.get_response()
async def export_history(chat: AsyncChat, message: Message):
doc = BytesIO(pickle.dumps(chat._curated_history))
doc.name = "AI_Chat_History.pkl"
caption = Response(await chat.send_message("Summarize our Conversation into one line.")).text()
await bot.send_document(chat_id=message.from_user.id, document=doc, caption=caption)

View File

@@ -0,0 +1,147 @@
import io
import logging
import wave
import numpy as np
from google.genai.client import AsyncClient, Client
from google.genai.types import Blob, GenerateContentResponse
from pyrogram.enums import ParseMode
from ub_core.utils import MediaExts
from app import CustomDB, extra_config
logging.getLogger("google_genai.models").setLevel(logging.WARNING)
DB_SETTINGS = CustomDB["COMMON_SETTINGS"]
try:
client: Client | None = Client(api_key=extra_config.GEMINI_API_KEY)
async_client: AsyncClient | None = client.aio
except:
client = async_client = None
class Response:
def __init__(self, ai_response: GenerateContentResponse):
self._ai_response = ai_response
self.first_candidate = None
self.first_content = None
self.first_parts = []
if ai_response.candidates:
self.first_candidate = ai_response.candidates[0]
if self.first_candidate.content:
self.first_content = self.first_candidate.content
if self.first_content.parts:
self.first_parts = self.first_content.parts
for part in self.first_parts:
if part.inline_data:
self._inline_data = part.inline_data
break
else:
self._inline_data = None
self.is_empty = not self.first_parts
self.failed_str = "`Error: Query Failed.`"
def wrap_in_quote(self, text: str, mode: ParseMode = ParseMode.MARKDOWN):
_text = text.strip()
match mode:
case ParseMode.MARKDOWN:
return _text if "```" in _text else f"**>\n{_text}<**"
case ParseMode.HTML:
return f"<blockquote expandable=true>{_text}</blockquote>"
case _:
return _text
@property
def _text(self) -> str:
return "\n".join(part.text for part in self.first_parts if isinstance(part.text, str))
def text(self, quote_mode: ParseMode | None = ParseMode.MARKDOWN) -> str:
if self.is_empty:
return self.failed_str
return self.wrap_in_quote(text=self._text, mode=quote_mode)
def text_with_sources(self, quote_mode: ParseMode = ParseMode.MARKDOWN) -> str:
if self.is_empty:
return self.failed_str
try:
chunks = self.first_candidate.grounding_metadata.grounding_chunks
except (AttributeError, TypeError):
return self.text(quote_mode=quote_mode)
hrefs = [f"[{chunk.web.title}]({chunk.web.uri})" for chunk in chunks]
sources = "\n\nSources: " + " | ".join(hrefs)
final_text = self._text.strip() + sources
return self.wrap_in_quote(text=final_text, mode=quote_mode)
@property
def image(self) -> bool:
if self._inline_data and self._inline_data.mime_type:
return "image" in self._inline_data.mime_type
return False
@property
def image_file(self) -> io.BytesIO | None:
inline_data = self._inline_data
if inline_data:
file = io.BytesIO(inline_data.data)
file.name = "photo.png"
return file
return None
@staticmethod
def save_wave_file(pcm, channels=1, rate=24000, sample_width=2) -> io.BytesIO:
file = io.BytesIO()
with wave.open(file, mode="wb") as wf:
wf.setnchannels(channels)
wf.setsampwidth(sample_width)
wf.setframerate(rate)
wf.writeframes(pcm)
n_samples = len(pcm) // (sample_width * channels)
duration = n_samples / rate
dtype = {1: np.int8, 2: np.int16, 4: np.int32}[sample_width]
samples = np.frombuffer(pcm, dtype=dtype)
chunk_size = max(1, len(samples) // 80)
waveform = bytes(
[
int(
min(
255,
np.abs(samples[i : i + chunk_size]).mean()
/ (2 ** (8 * sample_width - 1))
* 255,
)
)
for i in range(0, len(samples), chunk_size)
]
)
waveform = waveform[:80]
file.name = "audio.ogg"
file.waveform = waveform
file.duration = round(duration)
return file
@property
def audio(self) -> bool:
if self._inline_data and self._inline_data.mime_type:
return "audio" in self._inline_data.mime_type
return False
@property
def audio_file(self) -> io.BytesIO | None:
inline_data = self._inline_data
return self.save_wave_file(inline_data.data) if inline_data else None

View File

@@ -0,0 +1,134 @@
import logging
from google.genai import types
from ub_core import CustomDB
logging.getLogger("google_genai.models").setLevel(logging.WARNING)
DB_SETTINGS = CustomDB["COMMON_SETTINGS"]
async def init_task():
model_info = await DB_SETTINGS.find_one({"_id": "gemini_model_info"}) or {}
if model_name := model_info.get("model_name"):
AIConfig.TEXT_MODEL = model_name
if image_model := model_info.get("image_model_name"):
AIConfig.IMAGE_MODEL = image_model
SAFETY_SETTINGS = [
# SafetySetting(category="HARM_CATEGORY_UNSPECIFIED", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_HATE_SPEECH", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_DANGEROUS_CONTENT", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_HARASSMENT", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold="BLOCK_NONE"),
types.SafetySetting(category="HARM_CATEGORY_CIVIC_INTEGRITY", threshold="BLOCK_NONE"),
]
SEARCH_TOOL = types.Tool(
google_search=types.GoogleSearchRetrieval(
dynamic_retrieval_config=types.DynamicRetrievalConfig(dynamic_threshold=0.3)
)
)
SYSTEM_INSTRUCTION = (
"Answer precisely and in short unless specifically instructed otherwise."
"\nIF asked related to code, do not comment the code and do not explain the code unless instructed."
)
MALE_SPEECH_CONFIG = types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Puck")
)
)
FEMALE_SPEECH_CONFIG = types.SpeechConfig(
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Kore")
)
)
MULTI_SPEECH_CONFIG = types.SpeechConfig(
multi_speaker_voice_config=types.MultiSpeakerVoiceConfig(
speaker_voice_configs=[
types.SpeakerVoiceConfig(
speaker="John",
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(
voice_name="Kore",
)
),
),
types.SpeakerVoiceConfig(
speaker="Jane",
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(
voice_name="Puck",
)
),
),
]
)
)
class AIConfig:
TEXT_MODEL = "gemini-2.0-flash"
TEXT_CONFIG = types.GenerateContentConfig(
candidate_count=1,
# max_output_tokens=1024,
response_modalities=["Text"],
system_instruction=SYSTEM_INSTRUCTION,
temperature=0.69,
tools=[],
)
IMAGE_MODEL = "gemini-2.0-flash-exp"
IMAGE_CONFIG = types.GenerateContentConfig(
candidate_count=1,
# max_output_tokens=1024,
response_modalities=["Text", "Image"],
# system_instruction=SYSTEM_INSTRUCTION,
temperature=0.99,
)
AUDIO_MODEL = "gemini-2.5-flash-preview-tts"
AUDIO_CONFIG = types.GenerateContentConfig(
temperature=1,
response_modalities=["audio"],
speech_config=FEMALE_SPEECH_CONFIG,
)
@staticmethod
def get_kwargs(flags: list[str]) -> dict:
if "-i" in flags:
return {"model": AIConfig.IMAGE_MODEL, "config": AIConfig.IMAGE_CONFIG}
if "-a" in flags:
audio_config = AIConfig.AUDIO_CONFIG
if "-m" in flags:
audio_config.speech_config = MALE_SPEECH_CONFIG
else:
audio_config.speech_config = FEMALE_SPEECH_CONFIG
return {"model": AIConfig.AUDIO_MODEL, "config": audio_config}
if "-sp" in flags:
AIConfig.AUDIO_CONFIG.speech_config = MULTI_SPEECH_CONFIG
return {"model": AIConfig.AUDIO_MODEL, "config": AIConfig.AUDIO_CONFIG}
tools = AIConfig.TEXT_CONFIG.tools
use_search = "-s" in flags
if not use_search and SEARCH_TOOL in tools:
tools.remove(SEARCH_TOOL)
if use_search and SEARCH_TOOL not in tools:
tools.append(SEARCH_TOOL)
return {"model": AIConfig.TEXT_MODEL, "config": AIConfig.TEXT_CONFIG}

View File

@@ -0,0 +1,90 @@
from pyrogram.enums import ParseMode
from pyrogram.types import InputMediaAudio, InputMediaPhoto
from app import BOT, Message, bot
from app.plugins.ai.gemini import AIConfig, Response, async_client
from app.plugins.ai.gemini.utils import create_prompts, run_basic_check
@bot.add_cmd(cmd="ai")
@run_basic_check
async def question(bot: BOT, message: Message):
"""
CMD: AI
INFO: Ask a question to Gemini AI or get info about replied message / media.
FLAGS:
-s: to use Search
-i: to edit/generate images
-a: to generate audio
-m: male voice
-f: female voice
-sp: to create speech between two people
USAGE:
.ai what is the meaning of life.
.ai [reply to a message] (sends replied text as query)
.ai [reply to message] [extra prompt relating to replied text]
.ai [reply to image | video | gif]
.ai [reply to image | video | gif] [custom prompt]
.ai -a [-m|-f] <text to speak> (defaults to female voice)
.ai -sp TTS the following conversation between Joe and Jane:
Joe: How's it going today Jane?
Jane: Not too bad, how about you?
"""
reply = message.replied
prompt = message.filtered_input.strip()
if reply and reply.media:
resp_str = "<code>Processing... this may take a while.</code>"
else:
resp_str = "<code>Input received... generating response.</code>"
message_response = await message.reply(resp_str)
try:
prompts = await create_prompts(message=message)
except AssertionError as e:
await message_response.edit(e)
return
kwargs = AIConfig.get_kwargs(flags=message.flags)
response = await async_client.models.generate_content(contents=prompts, **kwargs)
response = Response(response)
text = response.text_with_sources()
if response.image:
await message_response.edit_media(
media=InputMediaPhoto(media=response.image_file, caption=f"**>\n•> {prompt}<**")
)
return
if response.audio:
if isinstance(message, Message):
await message.reply_voice(
voice=response.audio_file,
waveform=response.audio_file.waveform,
duration=response.audio_file.duration,
caption=f"**>\n•> {prompt}<**",
)
else:
await message_response.edit_media(
media=InputMediaAudio(
media=response.audio_file,
caption=f"**>\n•> {prompt}<**",
duration=response.audio_file.duration,
)
)
return
await message_response.edit(
text=f"**>\n•> {prompt}<**\n{text}",
parse_mode=ParseMode.MARKDOWN,
disable_preview=True,
)

View File

@@ -0,0 +1,151 @@
import asyncio
import shutil
import time
from functools import wraps
from mimetypes import guess_type
from google.genai.types import File, Part
from ub_core.utils import get_tg_media_details
from app import BOT, Message, extra_config
from app.plugins.ai.gemini import DB_SETTINGS, AIConfig, async_client
def run_basic_check(function):
@wraps(function)
async def wrapper(bot: BOT, message: Message):
if not extra_config.GEMINI_API_KEY:
await message.reply(
"Gemini API KEY not found."
"\nGet it <a href='https://makersuite.google.com/app/apikey'>HERE</a> "
"and set GEMINI_API_KEY var."
)
return
if not (message.input or message.replied):
await message.reply("<code>Ask a Question | Reply to a Message</code>")
return
await function(bot, message)
return wrapper
async def save_file(message: Message, check_size: bool = True) -> File | None:
media = get_tg_media_details(message)
if check_size:
assert getattr(media, "file_size", 0) <= 1048576 * 25, "File size exceeds 25mb."
download_dir = f"downloads/{time.time()}/"
try:
downloaded_file: str = await message.download(download_dir)
uploaded_file = await async_client.files.upload(
file=downloaded_file,
config={
"mime_type": getattr(media, "mime_type", None) or guess_type(downloaded_file)[0]
},
)
while uploaded_file.state.name == "PROCESSING":
await asyncio.sleep(5)
uploaded_file = await async_client.files.get(name=uploaded_file.name)
return uploaded_file
finally:
shutil.rmtree(download_dir, ignore_errors=True)
PROMPT_MAP = {
"video": "Summarize video and audio from the file",
"photo": "Summarize the image file",
"voice": (
"\nDo not summarise."
"\nTranscribe the audio file to english alphabets AS IS."
"\nTranslate it only if the audio is not english."
"\nIf the audio is in hindi: Transcribe it to hinglish without translating."
),
}
PROMPT_MAP["audio"] = PROMPT_MAP["voice"]
async def create_prompts(
message: Message, is_chat: bool = False, check_size: bool = True
) -> list[File, str] | list[Part]:
default_media_prompt = "Analyse the file and explain."
input_prompt = message.filtered_input or "answer"
# Conversational
if is_chat:
if message.media:
prompt = message.caption or PROMPT_MAP.get(message.media.value) or default_media_prompt
text_part = Part.from_text(text=prompt)
uploaded_file = await save_file(message=message, check_size=check_size)
file_part = Part.from_uri(file_uri=uploaded_file.uri, mime_type=uploaded_file.mime_type)
return [text_part, file_part]
return [Part.from_text(text=message.text)]
# Single Use
if reply := message.replied:
if reply.media:
prompt = (
message.filtered_input or PROMPT_MAP.get(reply.media.value) or default_media_prompt
)
text_part = Part.from_text(text=prompt)
uploaded_file = await save_file(message=reply, check_size=check_size)
file_part = Part.from_uri(file_uri=uploaded_file.uri, mime_type=uploaded_file.mime_type)
return [text_part, file_part]
return [Part.from_text(text=input_prompt), Part.from_text(text=str(reply.text))]
return [Part.from_text(text=input_prompt)]
@BOT.add_cmd(cmd="llms")
async def list_ai_models(bot: BOT, message: Message):
"""
CMD: LIST MODELS
INFO: List and change Gemini Models.
USAGE: .llms
"""
model_list = [
model.name.lstrip("models/")
async for model in await async_client.models.list(config={"query_base": True})
if "generateContent" in model.supported_actions
]
model_str = "\n\n".join(model_list)
update_str = (
f"<b>Current Model</b>: <code>"
f"{AIConfig.TEXT_MODEL if "-i" not in message.flags else AIConfig.IMAGE_MODEL}</code>"
f"\n\n<blockquote expandable=True><pre language=text>{model_str}</pre></blockquote>"
"\n\nReply to this message with the <code>model name</code> to change to a different model."
)
model_info_response = await message.reply(update_str)
model_response = await model_info_response.get_response(
timeout=60, reply_to_message_id=model_info_response.id, from_user=message.from_user.id
)
if not model_response:
await model_info_response.delete()
return
if model_response.text not in model_list:
await model_info_response.edit(f"<code>Invalid Model... Try again</code>")
return
if "-i" in message.flags:
data_key = "image_model_name"
AIConfig.IMAGE_MODEL = model_response.text
else:
data_key = "model_name"
AIConfig.TEXT_MODEL = model_response.text
await DB_SETTINGS.add_data({"_id": "gemini_model_info", data_key: model_response.text})
resp_str = f"{model_response.text} saved as model."
await model_info_response.edit(resp_str)
await bot.log_text(text=resp_str, type=f"ai_{data_key}")

170
app/plugins/ai/openai.py Normal file
View File

@@ -0,0 +1,170 @@
from base64 import b64decode
from io import BytesIO
from os import environ
import openai
from pyrogram.enums import ParseMode
from pyrogram.types import InputMediaPhoto
from app import BOT, Message
from app.plugins.ai.gemini.config import SYSTEM_INSTRUCTION
OPENAI_CLIENT = environ.get("OPENAI_CLIENT", "")
OPENAI_MODEL = environ.get("OPENAI_MODEL", "gpt-4o")
AI_CLIENT = getattr(openai, f"Async{OPENAI_CLIENT}OpenAI")
if AI_CLIENT == openai.AsyncAzureOpenAI:
text_init_kwargs = dict(
api_key=environ.get("AZURE_OPENAI_API_KEY"),
api_version=environ.get("OPENAI_API_VERSION"),
azure_endpoint=environ.get("AZURE_OPENAI_ENDPOINT"),
azure_deployment=environ.get("AZURE_DEPLOYMENT"),
)
image_init_kwargs = dict(
api_key=environ.get("DALL_E_API_KEY"),
api_version=environ.get("DALL_E_API_VERSION"),
azure_endpoint=environ.get("DALL_E_ENDPOINT"),
azure_deployment=environ.get("DALL_E_DEPLOYMENT"),
)
else:
text_init_kwargs = dict(
api_key=environ.get("OPENAI_API_KEY"), base_url=environ.get("OPENAI_BASE_URL")
)
image_init_kwargs = dict(
api_key=environ.get("DALL_E_API_KEY"), base_url=environ.get("DALL_E_ENDPOINT")
)
try:
TEXT_CLIENT = AI_CLIENT(**text_init_kwargs)
except:
TEXT_CLIENT = None
try:
DALL_E_CLIENT = AI_CLIENT(**image_init_kwargs)
except:
DALL_E_CLIENT = None
@BOT.add_cmd(cmd="gpt")
async def chat_gpt(bot: BOT, message: Message):
"""
CMD: GPT
INFO: Ask a question to chat gpt.
SETUP:
To use this command you need to set either of these vars.
For Default Client:
OPENAI_API_KEY = your API key
OPENAI_MODEL = model (optional, defaults to gpt-4o)
OPENAI_BASE_URL = a custom endpoint (optional)
For Azure Client:
OPENAI_CLIENT="Azure"
OPENAI_API_VERSION = your version
OPENAI_MODEL = your azure model
AZURE_OPENAI_API_KEY = your api key
AZURE_OPENAI_ENDPOINT = your azure endpoint
AZURE_DEPLOYMENT = your azure deployment
USAGE:
.gpt hi
.gpt [reply to a message]
"""
if TEXT_CLIENT is None:
await message.reply(f"OpenAI Creds not set or are invalid.\nCheck Help.")
return
reply_text = message.replied.text if message.replied else ""
prompt = f"{reply_text}\n\n\n{message.input}".strip()
if not prompt:
await message.reply("Ask a Question | Reply to a message.")
return
chat_completion = await TEXT_CLIENT.chat.completions.create(
messages=[
{"role": "system", "content": SYSTEM_INSTRUCTION},
{"role": "user", "content": prompt},
],
model=OPENAI_MODEL,
)
response = chat_completion.choices[0].message.content
await message.reply(text=f"**>\n••> {prompt}<**\n" + response, parse_mode=ParseMode.MARKDOWN)
@BOT.add_cmd(cmd="igen")
async def chat_gpt(bot: BOT, message: Message):
"""
CMD: IGEN
INFO: Generate Images using Dall-E.
SETUP:
To use this command you need to set either of these vars.
For Default Client:
DALL_E_API_KEY = your API key
DALL_E_ENDPOINT = a custom endpoint (optional)
For Azure Client:
OPENAI_CLIENT="Azure"
DALL_E_API_KEY = your api key
DALL_E_API_VERSION = your version
DALL_E_ENDPOINT = your azure endpoint
DALL_E_DEPLOYMENT = your azure deployment
FLAGS:
-v: for vivid style images (default)
-n: for less vivid and natural type of images
-s: to send with spoiler
-p: portrait output
-l: landscape output
USAGE:
.igen cats on moon
"""
if DALL_E_CLIENT is None:
await message.reply(f"OpenAI Creds not set or are invalid.\nCheck Help.")
return
prompt = message.filtered_input.strip()
if not prompt:
await message.reply("Give a prompt to generate image.")
return
response = await message.reply("Generating image...")
if "-p" in message.flags:
output_res = "1024x1792"
elif "-l" in message.flags:
output_res = "1792x1024"
else:
output_res = "1024x1024"
try:
generated_image = await DALL_E_CLIENT.images.generate(
model="dall-e-3",
prompt=prompt,
n=1,
size=output_res,
quality="hd",
response_format="b64_json",
style="natural" if "-n" in message.flags else "vivid",
)
except Exception:
await response.edit("Something went wrong... Check log channel.")
raise
image_io = BytesIO(b64decode(generated_image.data[0].b64_json))
image_io.name = "photo.png"
await response.edit_media(
InputMediaPhoto(
media=image_io, caption=f"**>\n{prompt}\n<**", has_spoiler="-s" in message.flags
)
)

View File

@@ -0,0 +1,258 @@
import asyncio
import aiohttp
import time
import json
from pathlib import Path
from ub_core.utils import Download, DownloadedFile, get_tg_media_details, progress
from app import BOT, Message, bot
class AnonFiles:
# Multiple anonymous file hosting services
SERVICES = {
"anonfiles": {
"upload_url": "https://api.anonfiles.com/upload",
"name": "AnonFiles",
"max_size": "20GB"
},
"bayfiles": {
"upload_url": "https://api.bayfiles.com/upload",
"name": "BayFiles",
"max_size": "20GB"
},
"letsupload": {
"upload_url": "https://api.letsupload.cc/upload",
"name": "LetsUpload",
"max_size": "10GB"
},
"filechan": {
"upload_url": "https://api.filechan.org/upload",
"name": "FileChan",
"max_size": "5GB"
}
}
def __init__(self):
self._session = None
async def get_session(self):
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
async def close_session(self):
if self._session and not self._session.closed:
await self._session.close()
async def upload_file(self, file_path: Path, service: str = "anonfiles"):
"""Upload file to anonymous file hosting service"""
session = await self.get_session()
if service not in self.SERVICES:
raise Exception(f"Unknown service: {service}")
service_info = self.SERVICES[service]
try:
with open(file_path, 'rb') as f:
data = aiohttp.FormData()
data.add_field('file', f, filename=file_path.name)
async with session.post(service_info["upload_url"], data=data) as response:
if response.status == 200:
result = await response.json()
if result.get('status') == True or result.get('success') == True:
file_data = result.get('data', {})
file_info = file_data.get('file', file_data)
return {
'service': service_info['name'],
'filename': file_info.get('metadata', {}).get('name') or file_path.name,
'size': file_info.get('metadata', {}).get('size', {}).get('bytes', file_path.stat().st_size),
'url': file_info.get('url', {}).get('full') or file_info.get('url'),
'short_url': file_info.get('url', {}).get('short'),
'download_url': file_info.get('download', {}).get('url'),
'delete_url': file_info.get('remove', {}).get('url'),
'id': file_info.get('id')
}
else:
error = result.get('error', {})
raise Exception(f"Upload failed: {error.get('message', 'Unknown error')}")
else:
error_text = await response.text()
raise Exception(f"Upload failed: {response.status} - {error_text}")
except Exception as e:
raise Exception(f"{service_info['name']} upload error: {str(e)}")
anonfiles = AnonFiles()
@bot.add_cmd(cmd="anonup")
async def anon_upload(bot: BOT, message: Message):
"""
CMD: ANONUP
INFO: Upload files to anonymous file hosting services
FLAGS: -s for service selection (anonfiles, bayfiles, letsupload, filechan)
USAGE:
.anonup [reply to media]
.anonup -s bayfiles [reply to media]
.anonup [url]
"""
response = await message.reply("🔄 <b>Processing upload request...</b>")
if not message.replied and not message.input:
await response.edit("❌ <b>No input provided!</b>\n\nReply to a media file or provide a URL.")
return
try:
# Parse service selection
service = "anonfiles"
input_text = message.filtered_input if message.filtered_input else message.input
if "-s" in message.flags:
parts = input_text.split() if input_text else []
if len(parts) >= 1 and parts[0] in anonfiles.SERVICES:
service = parts[0]
input_text = " ".join(parts[1:]) if len(parts) > 1 else ""
dl_dir = Path("downloads") / str(time.time())
# Handle replied media
if message.replied and message.replied.media:
await response.edit("📥 <b>Downloading media from Telegram...</b>")
tg_media = get_tg_media_details(message.replied)
file_name = tg_media.file_name or f"file_{int(time.time())}"
file_path = dl_dir / file_name
dl_dir.mkdir(parents=True, exist_ok=True)
await message.replied.download(
file_name=file_path,
progress=progress,
progress_args=(response, "Downloading from Telegram...", file_path)
)
# Handle URL input
elif input_text and input_text.strip():
url = input_text.strip()
if not url.startswith(('http://', 'https://')):
await response.edit("❌ <b>Invalid URL!</b>\nPlease provide a valid HTTP/HTTPS URL.")
return
await response.edit("📥 <b>Downloading from URL...</b>")
dl_obj = await Download.setup(
url=url,
dir=dl_dir,
message_to_edit=response
)
downloaded_file = await dl_obj.download()
file_path = downloaded_file.path
await dl_obj.close()
else:
await response.edit("❌ <b>No input provided!</b>\n\nReply to a media file or provide a URL.")
return
# Upload to selected service
service_name = anonfiles.SERVICES[service]['name']
await response.edit(f"📤 <b>Uploading to {service_name}...</b>")
file_info = await anonfiles.upload_file(file_path, service)
# Cleanup
if file_path.exists():
file_path.unlink()
if dl_dir.exists() and not any(dl_dir.iterdir()):
dl_dir.rmdir()
# Format response
size_mb = file_info['size'] / (1024 * 1024)
result_text = f"✅ <b>Successfully uploaded to {file_info['service']}!</b>\n\n"
result_text += f"📁 <b>File:</b> <code>{file_info['filename']}</code>\n"
result_text += f"📊 <b>Size:</b> {size_mb:.2f} MB\n"
result_text += f"🆔 <b>ID:</b> <code>{file_info.get('id', 'N/A')}</code>\n\n"
result_text += f"🔗 <b>Download URL:</b>\n<code>{file_info['url']}</code>\n\n"
if file_info.get('short_url'):
result_text += f"🔗 <b>Short URL:</b>\n<code>{file_info['short_url']}</code>\n\n"
if file_info.get('delete_url'):
result_text += f"🗑 <b>Delete URL:</b>\n<code>{file_info['delete_url']}</code>"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Upload failed!</b>\n\n<code>{str(e)}</code>")
finally:
await anonfiles.close_session()
@bot.add_cmd(cmd="anonservices")
async def anon_services(bot: BOT, message: Message):
"""
CMD: ANONSERVICES
INFO: List available anonymous file hosting services
USAGE: .anonservices
"""
services_text = f"📋 <b>Available Anonymous File Hosting Services</b>\n\n"
for service_id, service_info in anonfiles.SERVICES.items():
services_text += f"🔸 <b>{service_info['name']}</b>\n"
services_text += f" • ID: <code>{service_id}</code>\n"
services_text += f" • Max Size: {service_info['max_size']}\n\n"
services_text += f"💡 <b>Usage:</b>\n"
services_text += f"• <code>.anonup</code> - Upload to AnonFiles (default)\n"
services_text += f"• <code>.anonup -s bayfiles</code> - Upload to BayFiles\n"
services_text += f"• <code>.anonup -s letsupload</code> - Upload to LetsUpload\n"
services_text += f"• <code>.anonup -s filechan</code> - Upload to FileChan\n\n"
services_text += f" <b>Features:</b>\n"
services_text += f"• Anonymous uploads (no registration)\n"
services_text += f"• Large file support\n"
services_text += f"• Automatic cleanup\n"
services_text += f"• Delete URLs provided"
await message.reply(services_text)
@bot.add_cmd(cmd="anonhelp")
async def anon_help(bot: BOT, message: Message):
"""
CMD: ANONHELP
INFO: Show anonymous file hosting help
USAGE: .anonhelp
"""
help_text = f"📋 <b>Anonymous File Hosting Help</b>\n\n"
help_text += f"🚀 <b>Quick Start:</b>\n"
help_text += f"• Reply to any media: <code>.anonup</code>\n"
help_text += f"• Upload from URL: <code>.anonup https://example.com/file.zip</code>\n\n"
help_text += f"⚙️ <b>Service Selection:</b>\n"
help_text += f"• <code>.anonup -s bayfiles</code> - Use BayFiles\n"
help_text += f"• <code>.anonup -s letsupload</code> - Use LetsUpload\n"
help_text += f"• <code>.anonup -s filechan</code> - Use FileChan\n\n"
help_text += f"📊 <b>File Size Limits:</b>\n"
help_text += f"• AnonFiles & BayFiles: 20GB\n"
help_text += f"• LetsUpload: 10GB\n"
help_text += f"• FileChan: 5GB\n\n"
help_text += f"🔧 <b>Other Commands:</b>\n"
help_text += f"• <code>.anonservices</code> - List all services\n"
help_text += f"• <code>.anonhelp</code> - Show this help\n\n"
help_text += f"⚠️ <b>Important:</b>\n"
help_text += f"• Files are uploaded anonymously\n"
help_text += f"• Save delete URLs to remove files later\n"
help_text += f"• No account registration required"
await message.reply(help_text)

288
app/plugins/files/catbox.py Normal file
View File

@@ -0,0 +1,288 @@
import asyncio
import aiohttp
import time
from pathlib import Path
from ub_core.utils import Download, DownloadedFile, get_tg_media_details, progress
from app import BOT, Message, bot
class CatBox:
UPLOAD_URL = "https://catbox.moe/user/api.php"
LITTERBOX_URL = "https://litterbox.catbox.moe/resources/internals/api.php"
def __init__(self):
self._session = None
async def get_session(self):
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
async def close_session(self):
if self._session and not self._session.closed:
await self._session.close()
async def upload_file(self, file_path: Path, temporary: bool = False, expiry: str = "1h"):
"""
Upload file to CatBox or LitterBox (temporary)
Args:
file_path: Path to file to upload
temporary: If True, upload to LitterBox (temporary), else CatBox (permanent)
expiry: For LitterBox only - "1h", "12h", "24h", "72h"
"""
session = await self.get_session()
try:
if temporary:
url = self.LITTERBOX_URL
valid_expiry = ["1h", "12h", "24h", "72h"]
if expiry not in valid_expiry:
expiry = "1h"
data = aiohttp.FormData()
data.add_field('reqtype', 'fileupload')
data.add_field('time', expiry)
else:
url = self.UPLOAD_URL
data = aiohttp.FormData()
data.add_field('reqtype', 'fileupload')
with open(file_path, 'rb') as f:
data.add_field('fileToUpload', f, filename=file_path.name)
async with session.post(url, data=data) as response:
if response.status == 200:
result_url = (await response.text()).strip()
if result_url.startswith('http'):
return {
'url': result_url,
'filename': file_path.name,
'size': file_path.stat().st_size,
'temporary': temporary,
'expiry': expiry if temporary else None
}
else:
raise Exception(f"Upload failed: {result_url}")
else:
error_text = await response.text()
raise Exception(f"Upload failed: {response.status} - {error_text}")
except Exception as e:
raise Exception(f"CatBox upload error: {str(e)}")
catbox = CatBox()
@bot.add_cmd(cmd="catup")
async def catbox_upload(bot: BOT, message: Message):
"""
CMD: CATUP
INFO: Upload files to CatBox (permanent hosting)
USAGE:
.catup [reply to media]
.catup [url]
"""
response = await message.reply("🔄 <b>Processing upload request...</b>")
if not message.replied and not message.input:
await response.edit("❌ <b>No input provided!</b>\n\nReply to a media file or provide a URL.")
return
try:
dl_dir = Path("downloads") / str(time.time())
# Handle replied media
if message.replied and message.replied.media:
await response.edit("📥 <b>Downloading media from Telegram...</b>")
tg_media = get_tg_media_details(message.replied)
file_name = tg_media.file_name or f"file_{int(time.time())}"
file_path = dl_dir / file_name
dl_dir.mkdir(parents=True, exist_ok=True)
await message.replied.download(
file_name=file_path,
progress=progress,
progress_args=(response, "Downloading from Telegram...", file_path)
)
# Handle URL input
elif message.input:
url = message.input.strip()
if not url.startswith(('http://', 'https://')):
await response.edit("❌ <b>Invalid URL!</b>\nPlease provide a valid HTTP/HTTPS URL.")
return
await response.edit("📥 <b>Downloading from URL...</b>")
dl_obj = await Download.setup(
url=url,
dir=dl_dir,
message_to_edit=response
)
downloaded_file = await dl_obj.download()
file_path = downloaded_file.path
await dl_obj.close()
# Upload to CatBox
await response.edit("📤 <b>Uploading to CatBox...</b>")
file_info = await catbox.upload_file(file_path, temporary=False)
# Cleanup
if file_path.exists():
file_path.unlink()
if dl_dir.exists() and not any(dl_dir.iterdir()):
dl_dir.rmdir()
# Format response
size_mb = file_info['size'] / (1024 * 1024)
result_text = f"✅ <b>Successfully uploaded to CatBox!</b>\n\n"
result_text += f"📁 <b>File:</b> <code>{file_info['filename']}</code>\n"
result_text += f"📊 <b>Size:</b> {size_mb:.2f} MB\n"
result_text += f"🏠 <b>Hosting:</b> Permanent\n\n"
result_text += f"🔗 <b>URL:</b>\n<code>{file_info['url']}</code>"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Upload failed!</b>\n\n<code>{str(e)}</code>")
finally:
await catbox.close_session()
@bot.add_cmd(cmd="litterup")
async def litterbox_upload(bot: BOT, message: Message):
"""
CMD: LITTERUP
INFO: Upload files to LitterBox (temporary hosting)
FLAGS: -t for expiry time (1h, 12h, 24h, 72h)
USAGE:
.litterup [reply to media]
.litterup -t 24h [reply to media]
.litterup [url]
"""
response = await message.reply("🔄 <b>Processing upload request...</b>")
if not message.replied and not message.input:
await response.edit("❌ <b>No input provided!</b>\n\nReply to a media file or provide a URL.")
return
try:
# Parse expiry time
expiry = "1h"
input_text = message.filtered_input if message.filtered_input else message.input
if "-t" in message.flags:
parts = input_text.split()
if len(parts) >= 1 and parts[0] in ["1h", "12h", "24h", "72h"]:
expiry = parts[0]
input_text = " ".join(parts[1:]) if len(parts) > 1 else ""
dl_dir = Path("downloads") / str(time.time())
# Handle replied media
if message.replied and message.replied.media:
await response.edit("📥 <b>Downloading media from Telegram...</b>")
tg_media = get_tg_media_details(message.replied)
file_name = tg_media.file_name or f"file_{int(time.time())}"
file_path = dl_dir / file_name
dl_dir.mkdir(parents=True, exist_ok=True)
await message.replied.download(
file_name=file_path,
progress=progress,
progress_args=(response, "Downloading from Telegram...", file_path)
)
# Handle URL input
elif input_text and input_text.strip():
url = input_text.strip()
if not url.startswith(('http://', 'https://')):
await response.edit("❌ <b>Invalid URL!</b>\nPlease provide a valid HTTP/HTTPS URL.")
return
await response.edit("📥 <b>Downloading from URL...</b>")
dl_obj = await Download.setup(
url=url,
dir=dl_dir,
message_to_edit=response
)
downloaded_file = await dl_obj.download()
file_path = downloaded_file.path
await dl_obj.close()
else:
await response.edit("❌ <b>No input provided!</b>\n\nReply to a media file or provide a URL.")
return
# Upload to LitterBox
await response.edit("📤 <b>Uploading to LitterBox...</b>")
file_info = await catbox.upload_file(file_path, temporary=True, expiry=expiry)
# Cleanup
if file_path.exists():
file_path.unlink()
if dl_dir.exists() and not any(dl_dir.iterdir()):
dl_dir.rmdir()
# Format response
size_mb = file_info['size'] / (1024 * 1024)
result_text = f"✅ <b>Successfully uploaded to LitterBox!</b>\n\n"
result_text += f"📁 <b>File:</b> <code>{file_info['filename']}</code>\n"
result_text += f"📊 <b>Size:</b> {size_mb:.2f} MB\n"
result_text += f"⏰ <b>Expires:</b> {file_info['expiry']}\n"
result_text += f"🗑 <b>Hosting:</b> Temporary\n\n"
result_text += f"🔗 <b>URL:</b>\n<code>{file_info['url']}</code>"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Upload failed!</b>\n\n<code>{str(e)}</code>")
finally:
await catbox.close_session()
@bot.add_cmd(cmd="catboxhelp")
async def catbox_help(bot: BOT, message: Message):
"""
CMD: CATBOXHELP
INFO: Show CatBox and LitterBox help information
USAGE: .catboxhelp
"""
help_text = f"📋 <b>CatBox & LitterBox Commands</b>\n\n"
help_text += f"🏠 <b>CatBox (Permanent Hosting):</b>\n"
help_text += f"• <code>.catup</code> - Upload files permanently\n"
help_text += f"• No expiration, files stay forever\n"
help_text += f"• Max file size: 200MB\n\n"
help_text += f"🗑 <b>LitterBox (Temporary Hosting):</b>\n"
help_text += f"• <code>.litterup</code> - Upload files temporarily\n"
help_text += f"• <code>.litterup -t 24h</code> - Set expiry time\n"
help_text += f"• Available expiry: 1h, 12h, 24h, 72h\n"
help_text += f"• Max file size: 1GB\n\n"
help_text += f"💡 <b>Usage Examples:</b>\n"
help_text += f"• Reply to media: <code>.catup</code>\n"
help_text += f"• Upload from URL: <code>.catup https://example.com/file.zip</code>\n"
help_text += f"• Temporary with custom expiry: <code>.litterup -t 72h</code>\n\n"
help_text += f"⚠️ <b>Notes:</b>\n"
help_text += f"• CatBox for permanent storage\n"
help_text += f"• LitterBox for temporary sharing\n"
help_text += f"• Both services are anonymous"
await message.reply(help_text)

543
app/plugins/files/gdrive.py Normal file
View File

@@ -0,0 +1,543 @@
import asyncio
import json
import os
from collections import defaultdict
from functools import wraps
import aiohttp
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from pyrogram.enums import ParseMode
from ub_core import BOT, Config, CustomDB, Message, bot
from ub_core.utils import Download, get_tg_media_details, progress
DB = CustomDB["COMMON_SETTINGS"]
INSTRUCTIONS = """
Gdrive Credentials and Access token not found!
- Get credentials.json from: https://console.cloud.google.com
<blockquote>Steps:
• Enable google drive api.
• Setup consent screen.
• Select external app.
• Add yourself in audience.
• Go back.
• Add the Google drive scope in data access.
• Create a desktop app and download the json in credentials section.</blockquote>
- Upload this file to your saved messages and reply to it with .gsetup
"""
class Drive:
URL_TEMPLATE = "https://drive.google.com/file/d/{media_id}/view?usp=sharing"
FOLDER_MIME = "application/vnd.google-apps.folder"
SHORTCUT_MIME = "application/vnd.google-apps.shortcut"
DRIVE_ROOT_ID = os.getenv("DRIVE_ROOT_ID", "root")
def __init__(self):
self._aiohttp_session = None
self._progress_store: dict[str, dict[str, str | int | asyncio.Task]] = defaultdict(dict)
self._creds: Credentials | None = None
self.service = None
self.files = None
self.is_authenticated = False
async def async_init(self):
if self._aiohttp_session is None:
self._aiohttp_session = aiohttp.ClientSession()
Config.EXIT_TASKS.append(self._aiohttp_session.close)
await self.set_creds()
@property
def creds(self):
if (
isinstance(self._creds, Credentials)
and self._creds.expired
and self._creds.refresh_token
):
self._creds.refresh(Request())
asyncio.run_coroutine_threadsafe(
coro=DB.add_data(
{"_id": "drive_creds", "creds": json.loads(self._creds.to_json())}
),
loop=bot.loop,
)
bot.log.info("Gdrive Creds Auto-Refreshed")
return self._creds
@creds.setter
def creds(self, creds):
self._creds = creds
async def set_creds(self):
cred_data = await DB.find_one({"_id": "drive_creds"})
if not cred_data:
self.is_authenticated = False
return
self.creds = Credentials.from_authorized_user_info(
info=cred_data["creds"], scopes=["https://www.googleapis.com/auth/drive"]
)
self.service = build(
serviceName="drive", version="v3", credentials=self.creds, cache_discovery=False
)
self.files = self.service.files()
self.is_authenticated = True
def ensure_creds(self, func):
@wraps(func)
async def inner(bot: BOT, message: Message):
if not self.is_authenticated:
await message.reply(INSTRUCTIONS)
else:
await func(bot, message)
return inner
async def list_contents(
self,
_id: bool = False,
limit: int = 10,
file_only: bool = False,
folder_only: bool = False,
search_param: str | None = None,
) -> list[dict[str, str | int]]:
"""
:param _id: The ID of the folder to list files from.
:param limit: Number of results to fetch.
:param file_only: If True, only list files.
:param folder_only: If True, only list folders.
:param search_param: A string to search for in file/folder names.
:return: A list of dictionaries containing file/folder id, name and mimeType.
"""
return await asyncio.to_thread(self._list, _id, limit, file_only, folder_only, search_param)
async def upload_from_url(
self,
file_url: str,
is_encoded: bool = False,
folder_id: str = None,
message_to_edit: Message = None,
):
try:
file_id = await self._upload_from_url(file_url, is_encoded, folder_id, message_to_edit)
if file_id is not None:
return self.URL_TEMPLATE.format(media_id=file_id)
except Exception as e:
return f"Error:\n{e}"
finally:
store = self._progress_store.pop(file_url, {})
store["done"] = True
task = store.get("edit_task")
if isinstance(task, asyncio.Task):
task.cancel()
async def upload_from_telegram(
self, media_message: Message, message_to_edit: Message = None, folder_id: str = None
):
try:
file_id = await self._upload_from_telegram(media_message, message_to_edit, folder_id)
if file_id is not None:
return self.URL_TEMPLATE.format(media_id=file_id)
except Exception as e:
return f"Error:\n{e}"
finally:
store = self._progress_store.pop(message_to_edit.task_id, {})
store["done"] = True
task = store.get("edit_task")
if isinstance(task, asyncio.Task):
task.cancel()
def _list(
self,
_id: bool = False,
limit: int = 10,
file_only: bool = False,
folder_only: bool = False,
search_param: str | None = None,
) -> list[dict[str, str | int]]:
query_params = ["trashed=false"]
if folder_only:
query_params.append(f"mimeType = '{self.FOLDER_MIME}'")
elif file_only:
query_params.append(f"mimeType != '{self.FOLDER_MIME}'")
if search_param is not None:
if _id:
query_params.append(f"'{search_param}' in parents")
else:
query_params.append(f"name contains '{search_param}'")
else:
query_params.append(f"'{self.DRIVE_ROOT_ID}' in parents")
query = " and ".join(query_params)
files = []
fields = "nextPageToken, files(id, name, mimeType, shortcutDetails)"
result = self.files.list(q=query, pageSize=limit, fields=fields).execute()
files.extend(result.get("files", []))
while next_token := result.get("nextPageToken"):
if len(files) >= limit:
break
else:
file_limit = limit - len(files)
result = self.files.list(
q=query, pageSize=file_limit, fields=fields, pageToken=next_token
).execute()
files.extend(result.get("files", []))
return files[0:limit]
async def create_file(self, file_name: str, folder_id: str = None) -> str:
"""
:return: An url pointing to a location in drive.
"""
headers = {
"Authorization": f"Bearer {self.creds.token}",
"Content-Type": "application/json",
"X-Upload-Content-Type": "application/octet-stream",
}
async with self._aiohttp_session.post(
url="https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable",
json={"name": file_name, "parents": [folder_id or self.DRIVE_ROOT_ID]},
headers=headers,
) as resp:
if resp.status != 200:
text = await resp.text()
raise Exception(f"Initiate failed: {text}")
return resp.headers["Location"]
async def upload_chunk(self, location, headers, chunk) -> str | None:
async with self._aiohttp_session.put(location, headers=headers, data=chunk) as put:
if put.status == 308:
# Chunk accepted, not finished yet
return None
elif put.status in (200, 201):
# File finished
file = await put.json()
return file["id"]
else:
text = await put.text()
raise Exception(f"Chunk upload failed with {put.status}: {text}")
async def _upload_from_url(
self,
file_url: str,
is_encoded: bool = False,
folder_id: str = None,
message_to_edit: Message = None,
):
async with Download(url=file_url, dir="", is_encoded_url=is_encoded) as downloader:
store = self._progress_store[file_url]
store["size"] = downloader.size_bytes
store["done"] = False
store["uploaded_size"] = 0
store["edit_task"] = asyncio.create_task(
self.progress_worker(store, message_to_edit), name="url_drive_up_prog"
)
file_session = downloader.file_response_session
file_session.raise_for_status()
drive_location = await self.create_file(downloader.file_name, folder_id)
start = 0
buffer = b""
chunk_size = 524288
async for chunk in downloader.iter_chunks(chunk_size):
buffer += chunk
if len(buffer) < chunk_size:
continue
else:
chunk = buffer[:chunk_size]
end = start + len(chunk) - 1
put_headers = {
"Content-Range": f"bytes {start}-{end}/{downloader.size_bytes}",
"Authorization": f"Bearer {self.creds.token}",
}
file_id = await self.upload_chunk(drive_location, put_headers, chunk)
start += len(chunk)
store["uploaded_size"] += len(chunk)
buffer = buffer[chunk_size:]
if buffer:
end = start + len(buffer) - 1
put_headers = {
"Content-Range": f"bytes {start}-{end}/{downloader.size_bytes}",
"Authorization": f"Bearer {self.creds.token}",
}
file_id = await self.upload_chunk(drive_location, put_headers, buffer)
start = end + 1
store["uploaded_size"] = start
buffer = b""
store["done"] = True
return file_id
async def _upload_from_telegram(
self, media_message: Message, message_to_edit: Message = None, folder_id: str = None
):
media = get_tg_media_details(media_message)
store = self._progress_store[message_to_edit.task_id]
store["size"] = getattr(media, "file_size", 0)
store["done"] = False
store["uploaded_size"] = 0
store["edit_task"] = asyncio.create_task(
self.progress_worker(store, message_to_edit), name="tg_drive_up_prog"
)
start = 0
drive_location = await self.create_file(getattr(media, "file_name"), folder_id)
file_id = None
# noinspection PyTypeChecker
async for chunk in message_to_edit._client.stream_media(message=media_message):
end = start + len(chunk) - 1
headers = {
"Content-Range": f"bytes {start}-{end}/{getattr(media, "file_size", 0)}",
"Authorization": f"Bearer {self.creds.token}",
}
file_id = await self.upload_chunk(drive_location, headers, chunk)
start = end + 1
store["uploaded_size"] = end + 1
return file_id
@staticmethod
async def progress_worker(store: dict, message: Message):
if not isinstance(message, Message):
return
while not store["done"]:
await progress(
current_size=store["uploaded_size"],
total_size=store["size"] or 1,
response=message,
action_str="Uploading to Drive...",
)
await asyncio.sleep(5)
drive = Drive()
async def init_task():
await drive.async_init()
@BOT.add_cmd("gsetup")
async def gdrive_creds_setup(bot: BOT, message: Message):
"""
CMD: GSETUP
INFO: Generated and save O-Auth Creds Json to bot.
USAGE: .gsetup {reply to credentials.json file}
"""
try:
assert message.replied.document.file_name == "credentials.json"
except (AssertionError, AttributeError):
await message.reply("credentials.json not found.")
return
try:
cred_file = await message.replied.download(in_memory=True)
cred_file.seek(0)
flow = InstalledAppFlow.from_client_config(
json.load(cred_file),
["https://www.googleapis.com/auth/drive"],
)
flow.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
auth_url, state = flow.authorization_url(prompt="consent")
auth_message = await message.reply(
f"Please go to this URL and authorize:\n{auth_url}\n\nReply to this message with the code within 30 seconds.",
)
code_message = await auth_message.get_response(
from_user=message.from_user.id, reply_to_message_id=auth_message.id, timeout=30
)
await auth_message.delete()
if not code_message:
await message.reply("expired")
return
await code_message.delete()
flow.fetch_token(code=code_message.text)
await DB.add_data({"_id": "drive_creds", "creds": json.loads(flow.credentials.to_json())})
await drive.set_creds()
await message.reply("Creds Saved!")
except Exception as e:
await message.reply(e)
@BOT.add_cmd("agcreds")
async def set_drive_creds(bot: BOT, message: Message):
"""
CMD: AGCREDS
INFO: Add your pre generated O-Auth Creds Json to bot.
USAGE: .agcreds {data}
"""
creds = message.input.strip()
if not creds:
await message.reply("Enter Creds!!!")
return
try:
creds_json = json.loads(creds)
creds = Credentials.from_authorized_user_info(info=creds_json)
if creds.expired and creds.refresh_token:
creds.refresh(Request())
await DB.add_data({"_id": "drive_creds", "creds": json.loads(creds.to_json())})
await drive.set_creds()
await message.reply("Creds added!")
except Exception as e:
await message.reply(e)
@BOT.add_cmd("rgcreds")
async def remove_drive_creds(bot: BOT, message: Message):
response = await message.reply(
"Are you sure you want to delete drive creds?\nreply with y to continue"
)
resp = await response.get_response(from_user=message.from_user.id)
if not (resp and resp.text in ("y", "Y")):
await response.edit("Aborted!!!")
return
drive.is_authenticated = False
await DB.delete_data({"_id": "drive_creds"})
await response.edit("Creds Deleted Successfully!")
@BOT.add_cmd("gls")
@drive.ensure_creds
async def list_drive(bot: BOT, message: Message):
"""
CMD: GLS
INFO: List Files/Folders from Drive
FLAGS:
-f: list files only
-d: list dirs only
-id: list via folder id
-l: limit of results (10 by default)
USAGE:
.gls [-f|-d]
.gls [-f|-d] abc (lists files/folders matching abc in name)
.gls -id <folder id>
.gls [-f|-d] -l 20 (lists 20 results)
.gls -l 20 abc (tries to list 20 results containing abc in name)
"""
response = await message.reply("Listing...")
flags = message.flags
filtered_input_chunks = message.filtered_input.split(maxsplit=1)
kwargs = {
"_id": False,
"limit": 10,
"folder_only": False,
"file_only": False,
"search_param": None,
}
# Search by ID
if "-id" in flags:
kwargs["_id"] = True
# list folders
if "-d" in flags:
kwargs["folder_only"] = True
# list files
if "-f" in flags:
kwargs["file_only"] = True
# limit total number of results
if "-l" in flags:
kwargs["limit"] = int(filtered_input_chunks[0])
# search for specific files/dirs
if len(filtered_input_chunks) == 2:
kwargs["search_param"] = filtered_input_chunks[1]
else:
# search for specific files/dirs
kwargs["search_param"] = message.filtered_input.strip() or None
remote_files = await drive.list_contents(**kwargs)
if not remote_files:
await response.edit("No results found.")
return
folders = []
files = [""]
shortcuts = [""]
for file in remote_files:
url = drive.URL_TEMPLATE.format(media_id=file["id"])
mime = file["mimeType"]
if mime == drive.FOLDER_MIME:
folders.append(f"📁 <a href={url}>{file["name"]}</a>")
elif mime == drive.SHORTCUT_MIME:
shortcut_details = file.get("shortcutDetails", {})
target_id = shortcut_details.get("targetId")
if target_id:
url = drive.URL_TEMPLATE.format(media_id=target_id)
shortcuts.append(f"🔗 <a href={url}>{file["name"]}</a>")
else:
files.append(f"📄 <a href={url}>{file["name"]}</a>")
list_str = "Results:\n\n" + "\n".join(folders + shortcuts + files)
await response.edit(list_str, parse_mode=ParseMode.HTML)
@BOT.add_cmd(cmd="gup")
@drive.ensure_creds
async def upload_to_drive(bot: BOT, message: Message):
"""
CMD: GUP
INFO: Upload file to drive
FLAGS:
-id: folder id
-e: if the url is encoded
USAGE:
.gup [reply to a message | url]
.gup -id <folder id> [reply to a message | url]
"""
reply = message.replied
response = await message.reply("Checking Input...")
if reply and reply.media:
folder_id = message.filtered_input if "-id" in message.flags else None
upload_coro = drive.upload_from_telegram(reply, response, folder_id=folder_id)
elif message.filtered_input.startswith("http"):
if "-id" in message.flags:
folder_id, file_url = message.filtered_input.split(maxsplit=1)
else:
folder_id = None
file_url = message.filtered_input
upload_coro = drive.upload_from_url(
file_url=file_url,
is_encoded="-e" in message.flags,
folder_id=folder_id,
message_to_edit=response,
)
else:
await response.edit("Invalid Input!!!")
return
await response.edit(await upload_coro)

300
app/plugins/files/gofile.py Normal file
View File

@@ -0,0 +1,300 @@
import asyncio
import aiohttp
import time
import json
from pathlib import Path
from ub_core.utils import Download, DownloadedFile, get_tg_media_details, progress
from app import BOT, Message, bot
class GoFile:
BASE_URL = "https://api.gofile.io"
def __init__(self):
self._session = None
async def get_session(self):
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
async def close_session(self):
if self._session and not self._session.closed:
await self._session.close()
async def get_server(self):
"""Get the best server for uploading"""
session = await self.get_session()
try:
async with session.get(f"{self.BASE_URL}/getServer") as response:
if response.status == 200:
result = await response.json()
if result.get('status') == 'ok':
return result['data']['server']
raise Exception("Failed to get upload server")
except Exception as e:
raise Exception(f"Server selection error: {str(e)}")
async def upload_file(self, file_path: Path, folder_id: str = None):
"""Upload file to GoFile"""
session = await self.get_session()
try:
# Get upload server
server = await self.get_server()
upload_url = f"https://{server}.gofile.io/uploadFile"
# Prepare form data
data = aiohttp.FormData()
if folder_id:
data.add_field('folderId', folder_id)
with open(file_path, 'rb') as f:
data.add_field('file', f, filename=file_path.name)
async with session.post(upload_url, data=data) as response:
if response.status == 200:
result = await response.json()
if result.get('status') == 'ok':
file_data = result['data']
return {
'filename': file_path.name,
'size': file_path.stat().st_size,
'download_page': file_data.get('downloadPage'),
'code': file_data.get('code'),
'parent_folder': file_data.get('parentFolder'),
'file_id': file_data.get('fileId'),
'server': server,
'direct_link': file_data.get('directLink')
}
else:
raise Exception(f"Upload failed: {result.get('errorMessage', 'Unknown error')}")
else:
error_text = await response.text()
raise Exception(f"Upload failed: {response.status} - {error_text}")
except Exception as e:
raise Exception(f"GoFile upload error: {str(e)}")
async def get_folder_contents(self, folder_id: str):
"""Get contents of a GoFile folder"""
session = await self.get_session()
try:
async with session.get(f"{self.BASE_URL}/getContent?contentId={folder_id}") as response:
if response.status == 200:
result = await response.json()
if result.get('status') == 'ok':
return result['data']
else:
raise Exception(f"Failed to get folder contents: {result.get('errorMessage')}")
else:
raise Exception(f"Request failed: {response.status}")
except Exception as e:
raise Exception(f"Error getting folder contents: {str(e)}")
gofile = GoFile()
@bot.add_cmd(cmd="goup")
async def gofile_upload(bot: BOT, message: Message):
"""
CMD: GOUP
INFO: Upload files to GoFile
FLAGS: -f for folder ID
USAGE:
.goup [reply to media]
.goup -f [folder_id] [reply to media]
.goup [url]
"""
response = await message.reply("🔄 <b>Processing upload request...</b>")
if not message.replied and not message.input:
await response.edit("❌ <b>No input provided!</b>\n\nReply to a media file or provide a URL.")
return
try:
# Parse folder ID
folder_id = None
input_text = message.filtered_input if message.filtered_input else message.input
if "-f" in message.flags:
parts = input_text.split() if input_text else []
if len(parts) >= 1:
folder_id = parts[0]
input_text = " ".join(parts[1:]) if len(parts) > 1 else ""
dl_dir = Path("downloads") / str(time.time())
# Handle replied media
if message.replied and message.replied.media:
await response.edit("📥 <b>Downloading media from Telegram...</b>")
tg_media = get_tg_media_details(message.replied)
file_name = tg_media.file_name or f"file_{int(time.time())}"
file_path = dl_dir / file_name
dl_dir.mkdir(parents=True, exist_ok=True)
await message.replied.download(
file_name=file_path,
progress=progress,
progress_args=(response, "Downloading from Telegram...", file_path)
)
# Handle URL input
elif input_text and input_text.strip():
url = input_text.strip()
if not url.startswith(('http://', 'https://')):
await response.edit("❌ <b>Invalid URL!</b>\nPlease provide a valid HTTP/HTTPS URL.")
return
await response.edit("📥 <b>Downloading from URL...</b>")
dl_obj = await Download.setup(
url=url,
dir=dl_dir,
message_to_edit=response
)
downloaded_file = await dl_obj.download()
file_path = downloaded_file.path
await dl_obj.close()
else:
await response.edit("❌ <b>No input provided!</b>\n\nReply to a media file or provide a URL.")
return
# Upload to GoFile
await response.edit("📤 <b>Uploading to GoFile...</b>")
file_info = await gofile.upload_file(file_path, folder_id)
# Cleanup
if file_path.exists():
file_path.unlink()
if dl_dir.exists() and not any(dl_dir.iterdir()):
dl_dir.rmdir()
# Format response
size_mb = file_info['size'] / (1024 * 1024)
result_text = f"✅ <b>Successfully uploaded to GoFile!</b>\n\n"
result_text += f"📁 <b>File:</b> <code>{file_info['filename']}</code>\n"
result_text += f"📊 <b>Size:</b> {size_mb:.2f} MB\n"
result_text += f"🆔 <b>File ID:</b> <code>{file_info['file_id']}</code>\n"
result_text += f"📂 <b>Folder ID:</b> <code>{file_info['parent_folder']}</code>\n"
result_text += f"🌐 <b>Server:</b> {file_info['server']}\n\n"
result_text += f"🔗 <b>Download Page:</b>\n<code>{file_info['download_page']}</code>\n\n"
if file_info.get('direct_link'):
result_text += f"📎 <b>Direct Link:</b>\n<code>{file_info['direct_link']}</code>\n\n"
result_text += f"🔑 <b>Access Code:</b> <code>{file_info['code']}</code>"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Upload failed!</b>\n\n<code>{str(e)}</code>")
finally:
await gofile.close_session()
@bot.add_cmd(cmd="golist")
async def gofile_list(bot: BOT, message: Message):
"""
CMD: GOLIST
INFO: List contents of a GoFile folder
USAGE: .golist [folder_id]
"""
response = await message.reply("🔄 <b>Fetching folder contents...</b>")
if not message.input:
await response.edit("❌ <b>No folder ID provided!</b>\n\nProvide a GoFile folder ID.")
return
try:
folder_id = message.input.strip()
# Get folder contents
folder_data = await gofile.get_folder_contents(folder_id)
folder_name = folder_data.get('name', 'Unknown')
folder_type = folder_data.get('type', 'Unknown')
children = folder_data.get('children', {})
contents_text = f"📂 <b>GoFile Folder: {folder_name}</b>\n\n"
contents_text += f"🆔 <b>ID:</b> <code>{folder_id}</code>\n"
contents_text += f"📋 <b>Type:</b> {folder_type}\n"
contents_text += f"📊 <b>Items:</b> {len(children)}\n\n"
if children:
files_count = 0
folders_count = 0
for item_id, item_data in children.items():
item_type = item_data.get('type', 'unknown')
item_name = item_data.get('name', 'Unknown')
if item_type == 'file':
size = item_data.get('size', 0)
size_mb = size / (1024 * 1024) if size > 0 else 0
contents_text += f"📄 <code>{item_name}</code> ({size_mb:.2f} MB)\n"
files_count += 1
elif item_type == 'folder':
contents_text += f"📁 <code>{item_name}</code>\n"
folders_count += 1
contents_text += f"\n📊 <b>Summary:</b> {files_count} files, {folders_count} folders"
else:
contents_text += "📭 <b>Folder is empty</b>"
await response.edit(contents_text)
except Exception as e:
await response.edit(f"❌ <b>Failed to list folder contents!</b>\n\n<code>{str(e)}</code>")
finally:
await gofile.close_session()
@bot.add_cmd(cmd="gohelp")
async def gofile_help(bot: BOT, message: Message):
"""
CMD: GOHELP
INFO: Show GoFile help information
USAGE: .gohelp
"""
help_text = f"📋 <b>GoFile Commands Help</b>\n\n"
help_text += f"🚀 <b>Upload Commands:</b>\n"
help_text += f"• <code>.goup</code> - Upload files to GoFile\n"
help_text += f"• <code>.goup -f [folder_id]</code> - Upload to specific folder\n\n"
help_text += f"📂 <b>Management Commands:</b>\n"
help_text += f"• <code>.golist [folder_id]</code> - List folder contents\n"
help_text += f"• <code>.gohelp</code> - Show this help\n\n"
help_text += f"💡 <b>Usage Examples:</b>\n"
help_text += f"• Reply to media: <code>.goup</code>\n"
help_text += f"• Upload from URL: <code>.goup https://example.com/file.zip</code>\n"
help_text += f"• Upload to folder: <code>.goup -f abc123</code>\n"
help_text += f"• List folder: <code>.golist abc123</code>\n\n"
help_text += f"📊 <b>Features:</b>\n"
help_text += f"• Free file hosting\n"
help_text += f"• Large file support\n"
help_text += f"• Folder organization\n"
help_text += f"• Direct download links\n"
help_text += f"• No registration required\n\n"
help_text += f" <b>File Limits:</b>\n"
help_text += f"• Max file size varies by server\n"
help_text += f"• Generally supports files up to 5GB\n"
help_text += f"• Files stored for extended periods"
await message.reply(help_text)

500
app/plugins/files/leech.py Normal file
View File

@@ -0,0 +1,500 @@
import asyncio
import os
import time
import tempfile
import hashlib
from pathlib import Path
import subprocess
import aiohttp
from ub_core import BOT, Message
from ub_core.utils import progress
# Import pixeldrain functionality
try:
from .pixeldrain import pixeldrain
PIXELDRAIN_AVAILABLE = True
except ImportError:
PIXELDRAIN_AVAILABLE = False
LEECH_TYPE_MAP: dict[str, str] = {
"-p": "photo",
"-a": "audio",
"-v": "video",
"-g": "animation",
"-d": "document",
}
@BOT.add_cmd("l")
async def leech_urls_to_tg(bot: BOT, message: Message):
"""
CMD: L (leech)
INFO: Instantly Upload Media to TG from Links without Downloading.
FLAGS:
-p: photo
-a: audio
-v: video
-g: gif
-d: document
-s: to leech with spoiler
USAGE:
.l { flag } link | file_id
.l { flag } -s link | file_id
"""
try:
method_str = LEECH_TYPE_MAP.get(message.flags[0])
assert method_str and message.filtered_input
reply_method = getattr(message, f"reply_{method_str}")
kwargs = {method_str: message.filtered_input}
if "-s" in message.flags:
kwargs["has_spoiler"] = True
if "-g" in message.flags and bot.is_user:
kwargs["unsave"] = True
await reply_method(**kwargs)
except (IndexError, AssertionError):
await message.reply("Invalid Input.\nCheck Help!")
return
except Exception as exc:
await message.reply(exc)
return
class TorrentLeecher:
def __init__(self):
self.download_dir = Path("downloads/leech")
self.download_dir.mkdir(parents=True, exist_ok=True)
self.active_downloads = {}
async def check_aria2c(self) -> bool:
"""Check if aria2c is available"""
try:
result = subprocess.run(['aria2c', '--version'],
capture_output=True, text=True, timeout=5)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError):
return False
async def download_torrent_with_aria2c(self, torrent_path: Path, download_dir: Path,
progress_callback=None) -> list[Path]:
"""Download torrent using aria2c - improved version"""
# Create a subdirectory for this torrent's content
content_dir = download_dir / "content"
content_dir.mkdir(exist_ok=True)
cmd = [
'aria2c',
'--seed-time=0', # Don't seed after download
'--bt-max-peers=50',
'--max-connection-per-server=10',
'--split=10',
'--max-concurrent-downloads=5',
'--continue=true', # Resume downloads
'--max-tries=3',
'--retry-wait=3',
'--timeout=30',
'--dir', str(content_dir), # Download to content subdirectory
'--summary-interval=1', # Progress updates every second
str(torrent_path)
]
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # Combine stderr and stdout
text=True,
universal_newlines=True,
bufsize=1
)
download_id = hashlib.md5(str(torrent_path).encode()).hexdigest()[:8]
self.active_downloads[download_id] = {
'process': process,
'start_time': time.time(),
'last_output': ''
}
# Monitor process output for progress
while process.poll() is None:
if progress_callback:
elapsed = int(time.time() - self.active_downloads[download_id]['start_time'])
# Try to read some output for progress info
try:
# Read available output without blocking
import select
if hasattr(select, 'select'):
ready, _, _ = select.select([process.stdout], [], [], 0.1)
if ready:
line = process.stdout.readline()
if line:
self.active_downloads[download_id]['last_output'] = line.strip()
except:
pass
await progress_callback({
'status': 'Downloading torrent content...',
'elapsed': f"{elapsed//60}m {elapsed%60}s",
'details': self.active_downloads[download_id].get('last_output', '')[:100]
})
await asyncio.sleep(2)
# Get the final output
stdout, _ = process.communicate()
if process.returncode == 0:
# Find all downloaded files in the content directory
downloaded_files = []
for file_path in content_dir.rglob('*'):
if (file_path.is_file() and
not file_path.name.endswith(('.aria2', '.torrent')) and
file_path.stat().st_size > 0): # Only non-empty files
downloaded_files.append(file_path)
# If no files found in content dir, check the main download dir
if not downloaded_files:
for file_path in download_dir.rglob('*'):
if (file_path.is_file() and
not file_path.name.endswith(('.aria2', '.torrent')) and
file_path.stat().st_size > 0):
downloaded_files.append(file_path)
return downloaded_files
else:
error_msg = stdout if stdout else "Unknown aria2c error"
raise Exception(f"aria2c failed: {error_msg}")
async def download_http(self, url: str, download_dir: Path,
progress_callback=None) -> list[Path]:
"""Download file via HTTP"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status != 200:
raise Exception(f"Download failed: {response.status}")
# Better filename extraction
filename = url.split('/')[-1].split('?')[0] # Remove query params
if not filename or '.' not in filename:
filename = f"download_{int(time.time())}"
if 'content-disposition' in response.headers:
cd = response.headers['content-disposition']
if 'filename=' in cd:
filename = cd.split('filename=')[1].strip('"\'')
file_path = download_dir / filename
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with open(file_path, 'wb') as f:
async for chunk in response.content.iter_chunked(8192):
f.write(chunk)
downloaded += len(chunk)
if progress_callback and total_size > 0:
progress_pct = (downloaded / total_size) * 100
await progress_callback({
'status': f'Downloading... {progress_pct:.1f}%',
'downloaded': downloaded,
'total': total_size
})
return [file_path]
torrent_leecher = TorrentLeecher()
async def download_torrent_file(url: str) -> bytes:
"""Download torrent file from URL"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
return await response.read()
else:
raise Exception(f"Failed to download torrent: {response.status}")
@BOT.add_cmd("leech")
async def enhanced_leech(bot: BOT, message: Message):
"""
CMD: LEECH
INFO: Download torrents/files and upload to Telegram or PixelDrain
FLAGS:
-pd: Upload to PixelDrain after download
-tg: Upload to Telegram chat (default behavior)
-del: Delete local files after upload
-http: Force HTTP download (for direct downloads)
USAGE:
.leech [url] (upload to Telegram)
.leech -pd [url] (upload to PixelDrain)
.leech -pd -del [url] (upload to PixelDrain and delete local)
.leech -http [direct_url] (HTTP download)
"""
response = await message.reply("🔄 <b>Processing download request...</b>")
if not message.input:
await response.edit("❌ <b>No URL provided!</b>\n\n"
"<b>Usage:</b>\n"
"• <code>.leech [url]</code> - Upload to Telegram\n"
"• <code>.leech -pd [url]</code> - Upload to PixelDrain\n"
"• <code>.leech -http [direct_url]</code> - HTTP download")
return
url = message.filtered_input.strip()
upload_to_pixeldrain = "-pd" in message.flags
upload_to_telegram = "-tg" in message.flags or not upload_to_pixeldrain # Default to Telegram
delete_after_upload = "-del" in message.flags
force_http = "-http" in message.flags
if upload_to_pixeldrain and not PIXELDRAIN_AVAILABLE:
await response.edit("❌ <b>PixelDrain module not available!</b>")
return
try:
# Create download directory
download_id = hashlib.md5(url.encode()).hexdigest()[:8]
download_dir = torrent_leecher.download_dir / f"leech_{download_id}_{int(time.time())}"
download_dir.mkdir(parents=True, exist_ok=True)
# Progress callback
async def progress_callback(status):
try:
progress_text = f"📥 <b>Downloading...</b>\n\n"
progress_text += f"🌐 <b>URL:</b> <code>{url[:50]}...</code>\n"
progress_text += f"📊 <b>Status:</b> {status.get('status', 'Unknown')}\n"
progress_text += f"⏱ <b>Time:</b> {status.get('elapsed', 'N/A')}"
if 'details' in status and status['details']:
progress_text += f"\n📋 <b>Details:</b> <code>{status['details']}</code>"
if 'downloaded' in status and 'total' in status:
mb_down = status['downloaded'] / 1024 / 1024
mb_total = status['total'] / 1024 / 1024
progress_text += f"\n📊 <b>Progress:</b> {mb_down:.1f}/{mb_total:.1f} MB"
await response.edit(progress_text)
except:
pass
downloaded_files = []
# Determine download method
is_torrent = url.endswith('.torrent') and not force_http
if is_torrent and await torrent_leecher.check_aria2c():
# Download torrent file first
await response.edit("📥 <b>Downloading torrent file...</b>")
torrent_data = await download_torrent_file(url)
torrent_file = download_dir / "torrent.torrent"
with open(torrent_file, 'wb') as f:
f.write(torrent_data)
# Download using aria2c
await response.edit("🚀 <b>Starting torrent download...</b>")
downloaded_files = await torrent_leecher.download_torrent_with_aria2c(
torrent_file, download_dir, progress_callback
)
else:
# Use HTTP download
if is_torrent:
await response.edit("⚠️ <b>aria2c not found, using HTTP...</b>")
else:
await response.edit("📥 <b>Starting HTTP download...</b>")
downloaded_files = await torrent_leecher.download_http(
url, download_dir, progress_callback
)
if not downloaded_files:
await response.edit("❌ <b>No files were downloaded!</b>")
return
# Handle uploads
uploaded_results = []
if upload_to_telegram:
await response.edit("📤 <b>Uploading files to Telegram...</b>")
for i, file_path in enumerate(downloaded_files):
try:
file_size = file_path.stat().st_size
# Skip very large files for Telegram (>2GB limit)
if file_size > 2 * 1024 * 1024 * 1024:
uploaded_results.append({
'name': file_path.name,
'error': 'File too large for Telegram (>2GB)'
})
continue
# Update progress
await response.edit(f"📤 <b>Uploading to Telegram...</b>\n\n"
f"📁 <b>File {i+1}/{len(downloaded_files)}:</b> <code>{file_path.name}</code>\n"
f"📊 <b>Size:</b> {file_size / 1024 / 1024:.1f} MB")
# Upload to Telegram as document
with open(file_path, 'rb') as f:
await message.reply_document(
document=f,
file_name=file_path.name,
caption=f"📥 <b>Leeched from:</b>\n<code>{url}</code>\n\n"
f"📊 <b>Size:</b> {file_size / 1024 / 1024:.1f} MB",
progress=progress,
progress_args=(response, f"Uploading {file_path.name}...", file_path.name)
)
uploaded_results.append({
'name': file_path.name,
'size': file_size,
'status': 'success'
})
# Delete local file if requested
if delete_after_upload and file_path.exists():
file_path.unlink()
except Exception as e:
uploaded_results.append({
'name': file_path.name,
'error': str(e)
})
elif upload_to_pixeldrain:
await response.edit("📤 <b>Uploading files to PixelDrain...</b>")
for file_path in downloaded_files:
try:
if file_path.stat().st_size > 1024 * 1024 * 1024: # 1GB limit
uploaded_results.append({
'name': file_path.name,
'error': 'File too large (>1GB)'
})
continue
file_info = await pixeldrain.upload_file(file_path, response)
uploaded_results.append({
'name': file_path.name,
'url': file_info['url'],
'size': file_info['size']
})
if delete_after_upload and file_path.exists():
file_path.unlink()
except Exception as e:
uploaded_results.append({
'name': file_path.name,
'error': str(e)
})
# Clean up directory if empty
if delete_after_upload:
try:
if download_dir.exists() and not any(download_dir.iterdir()):
download_dir.rmdir()
except:
pass
# Format final response
total_size = sum(f.stat().st_size for f in downloaded_files if f.exists())
result_text = f"✅ <b>Leech completed!</b>\n\n"
result_text += f"📂 <b>Files Downloaded:</b> {len(downloaded_files)}\n"
result_text += f"📊 <b>Total Size:</b> {total_size / 1024 / 1024:.1f} MB\n\n"
if upload_to_telegram:
success_count = len([r for r in uploaded_results if 'status' in r and r['status'] == 'success'])
error_count = len([r for r in uploaded_results if 'error' in r])
result_text += f"📤 <b>Telegram Upload:</b> {success_count} success, {error_count} failed\n\n"
if error_count > 0:
result_text += f"❌ <b>Failed uploads:</b>\n"
for result in uploaded_results:
if 'error' in result:
result_text += f"• <code>{result['name']}</code>: {result['error']}\n"
elif upload_to_pixeldrain:
result_text += f"🔗 <b>PixelDrain Links:</b>\n"
for result in uploaded_results:
if 'error' in result:
result_text += f"❌ <code>{result['name']}</code>: {result['error']}\n"
else:
size_mb = result['size'] / 1024 / 1024
result_text += f"✅ <code>{result['name']}</code> ({size_mb:.1f} MB)\n"
result_text += f" {result['url']}\n\n"
else:
result_text += f"📍 <b>Local Path:</b> <code>{download_dir}</code>\n\n"
result_text += f"📋 <b>Files:</b>\n"
for file_path in downloaded_files[:5]:
if file_path.exists():
size_mb = file_path.stat().st_size / 1024 / 1024
result_text += f"• <code>{file_path.name}</code> ({size_mb:.1f} MB)\n"
if len(downloaded_files) > 5:
result_text += f"... and {len(downloaded_files) - 5} more files"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Download failed!</b>\n\n<code>{str(e)}</code>")
@BOT.add_cmd("leechhelp")
async def leech_help(bot: BOT, message: Message):
"""
CMD: LEECHHELP
INFO: Show leech commands help
USAGE: .leechhelp
"""
help_text = f"📋 <b>Leech Commands Help</b>\n\n"
help_text += f"🔸 <b>Media Leech (Telegram):</b>\n"
help_text += f"• <code>.l -p [url]</code> - Upload as photo\n"
help_text += f"• <code>.l -v [url]</code> - Upload as video\n"
help_text += f"• <code>.l -a [url]</code> - Upload as audio\n"
help_text += f"• <code>.l -d [url]</code> - Upload as document\n"
help_text += f"• <code>.l -g [url]</code> - Upload as GIF\n\n"
help_text += f"🔸 <b>File Leech (Download + Upload):</b>\n"
help_text += f"• <code>.leech [url]</code> - Download and upload to Telegram\n"
help_text += f"• <code>.leech -pd [url]</code> - Download and upload to PixelDrain\n"
help_text += f"• <code>.leech -pd -del [url]</code> - Upload to PixelDrain and delete local\n"
help_text += f"• <code>.leech -http [url]</code> - Force HTTP download\n"
help_text += f"• <code>.leech -del [url]</code> - Upload to Telegram and delete local\n\n"
help_text += f"💡 <b>Examples:</b>\n"
help_text += f"• <code>.l -v https://site.com/video.mp4</code> (Direct to Telegram)\n"
help_text += f"• <code>.leech https://site.com/movie.torrent</code> (Torrent to Telegram)\n"
help_text += f"• <code>.leech -pd https://site.com/file.zip</code> (HTTP to PixelDrain)\n\n"
help_text += f"⚙️ <b>Requirements:</b>\n"
help_text += f"• aria2c (for torrent support)\n"
help_text += f"• PixelDrain module (for -pd flag)\n\n"
help_text += f"📊 <b>Features:</b>\n"
help_text += f"• Torrent and HTTP downloads\n"
help_text += f"• Direct Telegram upload (default)\n"
help_text += f"• PixelDrain integration\n"
help_text += f"• Progress tracking\n"
help_text += f"• File cleanup options\n"
help_text += f"• aria2c auto-detection\n"
help_text += f"• File size limits (2GB for Telegram, 1GB for PixelDrain)"
await message.reply(help_text)

View File

@@ -0,0 +1,325 @@
import asyncio
import aiohttp
import time
from pathlib import Path
from ub_core.utils import Download, DownloadedFile, get_tg_media_details, progress
from app import BOT, Message, bot
class PixelDrain:
BASE_URL = "https://pixeldrain.com"
API_URL = f"{BASE_URL}/api"
def __init__(self):
self._session = None
async def get_session(self):
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession()
return self._session
async def close_session(self):
if self._session and not self._session.closed:
await self._session.close()
async def upload_file(self, file_path: Path, message_to_edit: Message = None):
"""Upload file to PixelDrain"""
session = await self.get_session()
try:
file_size = file_path.stat().st_size
uploaded = 0
if message_to_edit:
await message_to_edit.edit("📤 <b>Uploading to PixelDrain...</b>")
with open(file_path, 'rb') as f:
data = aiohttp.FormData()
data.add_field('file', f, filename=file_path.name)
async with session.post(f"{self.API_URL}/file", data=data) as response:
if response.status == 201:
result = await response.json()
file_id = result.get('id')
file_info = {
'id': file_id,
'name': result.get('name', file_path.name),
'size': result.get('size', file_size),
'views': result.get('views', 0),
'url': f"{self.BASE_URL}/u/{file_id}",
'direct_url': f"{self.BASE_URL}/api/file/{file_id}",
'delete_url': f"{self.BASE_URL}/api/file/{file_id}"
}
return file_info
else:
error_text = await response.text()
raise Exception(f"Upload failed: {response.status} - {error_text}")
except Exception as e:
raise Exception(f"PixelDrain upload error: {str(e)}")
async def get_file_info(self, file_id: str):
"""Get file information from PixelDrain"""
session = await self.get_session()
try:
async with session.get(f"{self.API_URL}/file/{file_id}/info") as response:
if response.status == 200:
return await response.json()
else:
raise Exception(f"Failed to get file info: {response.status}")
except Exception as e:
raise Exception(f"Error getting file info: {str(e)}")
async def download_file(self, file_id: str, download_dir: Path, message_to_edit: Message = None):
"""Download file from PixelDrain"""
session = await self.get_session()
try:
# Get file info first
file_info = await self.get_file_info(file_id)
file_name = file_info.get('name', f'pixeldrain_{file_id}')
file_size = file_info.get('size', 0)
download_path = download_dir / file_name
download_dir.mkdir(parents=True, exist_ok=True)
if message_to_edit:
await message_to_edit.edit(f"📥 <b>Downloading from PixelDrain...</b>\n<code>{file_name}</code>")
async with session.get(f"{self.API_URL}/file/{file_id}") as response:
if response.status == 200:
downloaded = 0
with open(download_path, 'wb') as f:
async for chunk in response.content.iter_chunked(8192):
f.write(chunk)
downloaded += len(chunk)
if message_to_edit and file_size > 0:
progress_percent = (downloaded / file_size) * 100
await progress(downloaded, file_size, message_to_edit,
f"Downloading from PixelDrain...\n{file_name}")
return DownloadedFile(file=download_path, size=file_size)
else:
raise Exception(f"Download failed: {response.status}")
except Exception as e:
raise Exception(f"PixelDrain download error: {str(e)}")
pixeldrain = PixelDrain()
@bot.add_cmd(cmd="pdup")
async def pixeldrain_upload(bot: BOT, message: Message):
"""
CMD: PDUP
INFO: Upload files to PixelDrain
USAGE:
.pdup [reply to media]
.pdup [url]
"""
response = await message.reply("🔄 <b>Processing upload request...</b>")
if not message.replied and not message.input:
await response.edit("❌ <b>No input provided!</b>\n\nReply to a media file or provide a URL.")
return
try:
dl_dir = Path("downloads") / str(time.time())
# Handle replied media
if message.replied and message.replied.media:
await response.edit("📥 <b>Downloading media from Telegram...</b>")
tg_media = get_tg_media_details(message.replied)
file_name = tg_media.file_name or f"file_{int(time.time())}"
file_path = dl_dir / file_name
dl_dir.mkdir(parents=True, exist_ok=True)
await message.replied.download(
file_name=file_path,
progress=progress,
progress_args=(response, "Downloading from Telegram...", file_path)
)
# Handle URL input
elif message.input:
url = message.input.strip()
if not url.startswith(('http://', 'https://')):
await response.edit("❌ <b>Invalid URL!</b>\nPlease provide a valid HTTP/HTTPS URL.")
return
await response.edit("📥 <b>Downloading from URL...</b>")
dl_obj = await Download.setup(
url=url,
dir=dl_dir,
message_to_edit=response
)
downloaded_file = await dl_obj.download()
file_path = downloaded_file.path
await dl_obj.close()
# Upload to PixelDrain
await response.edit("📤 <b>Uploading to PixelDrain...</b>")
file_info = await pixeldrain.upload_file(file_path, response)
# Cleanup
if file_path.exists():
file_path.unlink()
if dl_dir.exists() and not any(dl_dir.iterdir()):
dl_dir.rmdir()
# Format response
size_mb = file_info['size'] / (1024 * 1024)
result_text = f"✅ <b>Successfully uploaded to PixelDrain!</b>\n\n"
result_text += f"📁 <b>File:</b> <code>{file_info['name']}</code>\n"
result_text += f"📊 <b>Size:</b> {size_mb:.2f} MB\n"
result_text += f"🆔 <b>ID:</b> <code>{file_info['id']}</code>\n"
result_text += f"👁 <b>Views:</b> {file_info['views']}\n\n"
result_text += f"🔗 <b>Share URL:</b>\n<code>{file_info['url']}</code>\n\n"
result_text += f"📎 <b>Direct URL:</b>\n<code>{file_info['direct_url']}</code>"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Upload failed!</b>\n\n<code>{str(e)}</code>")
finally:
await pixeldrain.close_session()
@bot.add_cmd(cmd="pddl")
async def pixeldrain_download(bot: BOT, message: Message):
"""
CMD: PDDL
INFO: Download files from PixelDrain
USAGE:
.pddl [pixeldrain_url_or_id]
"""
response = await message.reply("🔄 <b>Processing download request...</b>")
if not message.input:
await response.edit("❌ <b>No input provided!</b>\n\nProvide a PixelDrain URL or file ID.")
return
try:
input_text = message.input.strip()
# Extract file ID from URL or use direct ID
if 'pixeldrain.com' in input_text:
if '/u/' in input_text:
file_id = input_text.split('/u/')[-1].split('?')[0]
elif '/api/file/' in input_text:
file_id = input_text.split('/api/file/')[-1].split('/')[0]
else:
await response.edit("❌ <b>Invalid PixelDrain URL!</b>\n\nUse format: https://pixeldrain.com/u/[file_id]")
return
else:
file_id = input_text
# Validate file ID
if not file_id or len(file_id) < 8:
await response.edit("❌ <b>Invalid file ID!</b>\n\nFile ID should be at least 8 characters long.")
return
# Download file
dl_dir = Path("downloads") / str(time.time())
downloaded_file = await pixeldrain.download_file(file_id, dl_dir, response)
# Send file back to user
with open(downloaded_file.path, 'rb') as f:
await message.reply_document(
document=f,
file_name=downloaded_file.path.name,
caption=f"📥 <b>Downloaded from PixelDrain</b>\n\n"
f"🆔 <b>File ID:</b> <code>{file_id}</code>\n"
f"📊 <b>Size:</b> {downloaded_file.size / (1024 * 1024):.2f} MB"
)
await response.delete()
# Cleanup
if downloaded_file.path.exists():
downloaded_file.path.unlink()
if dl_dir.exists() and not any(dl_dir.iterdir()):
dl_dir.rmdir()
except Exception as e:
await response.edit(f"❌ <b>Download failed!</b>\n\n<code>{str(e)}</code>")
finally:
await pixeldrain.close_session()
@bot.add_cmd(cmd="pdinfo")
async def pixeldrain_info(bot: BOT, message: Message):
"""
CMD: PDINFO
INFO: Get information about a PixelDrain file
USAGE:
.pdinfo [pixeldrain_url_or_id]
"""
response = await message.reply("🔄 <b>Fetching file information...</b>")
if not message.input:
await response.edit("❌ <b>No input provided!</b>\n\nProvide a PixelDrain URL or file ID.")
return
try:
input_text = message.input.strip()
# Extract file ID from URL or use direct ID
if 'pixeldrain.com' in input_text:
if '/u/' in input_text:
file_id = input_text.split('/u/')[-1].split('?')[0]
elif '/api/file/' in input_text:
file_id = input_text.split('/api/file/')[-1].split('/')[0]
else:
await response.edit("❌ <b>Invalid PixelDrain URL!</b>")
return
else:
file_id = input_text
# Get file info
file_info = await pixeldrain.get_file_info(file_id)
# Format file size
size_bytes = file_info.get('size', 0)
if size_bytes > 1024 * 1024 * 1024:
size_str = f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
elif size_bytes > 1024 * 1024:
size_str = f"{size_bytes / (1024 * 1024):.2f} MB"
elif size_bytes > 1024:
size_str = f"{size_bytes / 1024:.2f} KB"
else:
size_str = f"{size_bytes} B"
# Format upload date
upload_date = file_info.get('date_upload', 'Unknown')
if upload_date != 'Unknown':
upload_date = upload_date.replace('T', ' ').split('.')[0]
info_text = f"📋 <b>PixelDrain File Information</b>\n\n"
info_text += f"🆔 <b>ID:</b> <code>{file_id}</code>\n"
info_text += f"📁 <b>Name:</b> <code>{file_info.get('name', 'Unknown')}</code>\n"
info_text += f"📊 <b>Size:</b> {size_str}\n"
info_text += f"🎭 <b>MIME:</b> <code>{file_info.get('mime_type', 'Unknown')}</code>\n"
info_text += f"👁 <b>Views:</b> {file_info.get('views', 0)}\n"
info_text += f"📅 <b>Uploaded:</b> {upload_date}\n\n"
info_text += f"🔗 <b>Share URL:</b>\n<code>https://pixeldrain.com/u/{file_id}</code>\n\n"
info_text += f"📎 <b>Direct URL:</b>\n<code>https://pixeldrain.com/api/file/{file_id}</code>"
await response.edit(info_text)
except Exception as e:
await response.edit(f"❌ <b>Failed to get file info!</b>\n\n<code>{str(e)}</code>")
finally:
await pixeldrain.close_session()

View File

@@ -0,0 +1,65 @@
import asyncio
import shutil
import time
from pathlib import Path
from ub_core.utils.downloader import Download, DownloadedFile
from app import BOT, Message, bot
from app.plugins.files.download import telegram_download
from app.plugins.files.upload import upload_to_tg
@bot.add_cmd(cmd="rename")
async def rename(bot: BOT, message: Message):
"""
CMD: RENAME
INFO: Upload Files with custom name
FLAGS: -s for spoiler
USAGE:
.rename [ url | reply to message ] file_name.ext
"""
input = message.filtered_input
response = await message.reply("Checking input...")
if not message.replied or not message.replied.media or not message.filtered_input:
await response.edit(
"Invalid input...\nReply to a message containing media or give a link and a filename with cmd."
)
return
dl_path = Path("downloads") / str(time.time())
await response.edit("Input verified....Starting Download...")
if message.replied:
dl_obj: None = None
download_coro = telegram_download(
message=message.replied, dir_name=dl_path, file_name=input, response=response
)
else:
url, file_name = input.split(maxsplit=1)
dl_obj: Download = await Download.setup(
url=url, dir=dl_path, message_to_edit=response, custom_file_name=file_name
)
download_coro = dl_obj.download()
try:
downloaded_file: DownloadedFile = await download_coro
await upload_to_tg(file=downloaded_file, message=message, response=response)
shutil.rmtree(dl_path, ignore_errors=True)
except asyncio.exceptions.CancelledError:
await response.edit("Cancelled....")
except TimeoutError:
await response.edit("Download Timeout...")
except Exception as e:
await response.edit(str(e))
finally:
if dl_obj:
await dl_obj.close()

View File

@@ -0,0 +1,34 @@
from pyrogram.enums import MessageMediaType
from ub_core import BOT, Message
from ub_core.utils import get_tg_media_details
MEDIA_TYPE_MAP: dict[MessageMediaType, str] = {
MessageMediaType.PHOTO: "photo",
MessageMediaType.VIDEO: "video",
}
@BOT.add_cmd("spoiler")
async def mark_spoiler(bot: BOT, message: Message):
"""
CMD: SPOILER
INFO: Convert Non-Spoiler media to Spoiler
USAGE: .spoiler [reply to a photo | video]
"""
reply_message = message.replied
try:
reply_method_str = MEDIA_TYPE_MAP.get(reply_message.media)
assert reply_method_str and not reply_message.document
except (AssertionError, AttributeError):
await message.reply(text="Reply to a Photo | Video")
return
media = get_tg_media_details(message=reply_message)
kwargs = {reply_method_str: media.file_id, "has_spoiler": True}
reply_method = getattr(message, f"reply_{reply_method_str}")
await reply_method(**kwargs)

View File

@@ -0,0 +1,406 @@
import asyncio
import os
import time
import tempfile
import hashlib
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, List
import aiohttp
from ub_core.utils import progress
from app import BOT, Message, bot
# Try to import libtorrent, make it optional
try:
import libtorrent as lt
LIBTORRENT_AVAILABLE = True
except ImportError:
LIBTORRENT_AVAILABLE = False
lt = None
# Import pixeldrain functionality
try:
from .pixeldrain import pixeldrain
PIXELDRAIN_AVAILABLE = True
except ImportError:
PIXELDRAIN_AVAILABLE = False
class TorrentLeecher:
def __init__(self):
self.session = lt.session()
self.session.listen_on(6881, 6891)
self.active_torrents: Dict[str, Dict] = {}
self.download_dir = Path("downloads/torrents")
self.download_dir.mkdir(parents=True, exist_ok=True)
def add_torrent(self, torrent_data: bytes, download_path: Path) -> str:
"""Add torrent to session and return info hash"""
if not LIBTORRENT_AVAILABLE:
raise Exception("libtorrent not available - install python-libtorrent")
info = lt.torrent_info(torrent_data)
handle = self.session.add_torrent({
'ti': info,
'save_path': str(download_path.parent),
'storage_mode': lt.storage_mode_t.storage_mode_sparse,
})
info_hash = str(info.info_hash())
self.active_torrents[info_hash] = {
'handle': handle,
'info': info,
'start_time': time.time(),
'download_path': download_path,
'completed': False
}
return info_hash
def get_torrent_status(self, info_hash: str) -> Optional[Dict]:
"""Get status of a torrent"""
if info_hash not in self.active_torrents:
return None
handle = self.active_torrents[info_hash]['handle']
status = handle.status()
return {
'name': self.active_torrents[info_hash]['info'].name(),
'progress': status.progress,
'download_rate': status.download_rate,
'upload_rate': status.upload_rate,
'num_peers': status.num_peers,
'num_seeds': status.num_seeds,
'total_size': self.active_torrents[info_hash]['info'].total_size(),
'downloaded': status.total_done,
'eta': self._calculate_eta(status),
'state': str(status.state),
'completed': status.is_finished
}
def _calculate_eta(self, status) -> str:
"""Calculate estimated time of arrival"""
if status.download_rate <= 0:
return ""
remaining = status.total_wanted - status.total_done
eta_seconds = remaining / status.download_rate
if eta_seconds < 60:
return f"{int(eta_seconds)}s"
elif eta_seconds < 3600:
return f"{int(eta_seconds/60)}m"
else:
return f"{int(eta_seconds/3600)}h {int((eta_seconds%3600)/60)}m"
async def download_torrent(self, info_hash: str, progress_callback=None) -> List[Path]:
"""Download torrent and return list of downloaded files"""
if info_hash not in self.active_torrents:
raise Exception("Torrent not found")
handle = self.active_torrents[info_hash]['handle']
# Wait for torrent to complete
while not handle.status().is_finished:
status = self.get_torrent_status(info_hash)
if progress_callback:
await progress_callback(status)
await asyncio.sleep(2)
# Get list of downloaded files
info = self.active_torrents[info_hash]['info']
base_path = self.active_torrents[info_hash]['download_path']
downloaded_files = []
for i in range(info.num_files()):
file_info = info.file_at(i)
file_path = base_path / file_info.path
if file_path.exists():
downloaded_files.append(file_path)
self.active_torrents[info_hash]['completed'] = True
return downloaded_files
def remove_torrent(self, info_hash: str, delete_files: bool = False):
"""Remove torrent from session"""
if info_hash in self.active_torrents:
handle = self.active_torrents[info_hash]['handle']
if delete_files:
self.session.remove_torrent(handle, lt.options_t.delete_files)
else:
self.session.remove_torrent(handle)
del self.active_torrents[info_hash]
torrent_leecher = TorrentLeecher()
async def download_torrent_file(url: str) -> bytes:
"""Download torrent file from URL"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
return await response.read()
else:
raise Exception(f"Failed to download torrent: {response.status}")
def parse_magnet_link(magnet_url: str) -> bytes:
"""Convert magnet link to torrent data (simplified)"""
# This is a basic implementation - in practice, you'd need to resolve the magnet link
# For now, we'll raise an exception asking for .torrent file
raise Exception("Magnet links not supported yet. Please provide a .torrent file URL.")
@bot.add_cmd(cmd="torrent")
async def torrent_leech(bot: BOT, message: Message):
"""
CMD: TORRENT
INFO: Download torrents and optionally upload to PixelDrain (requires libtorrent)
FLAGS:
-pd: Upload to PixelDrain after download
-del: Delete local files after upload
USAGE:
.torrent [torrent_url]
.torrent -pd [torrent_url]
.torrent -pd -del [torrent_url]
"""
response = await message.reply("🔄 <b>Processing torrent request...</b>")
if not LIBTORRENT_AVAILABLE:
await response.edit("❌ <b>LibTorrent not available!</b>\n\n"
"Install python-libtorrent to use torrent functionality.\n"
"Use <code>.leech</code> command instead for aria2c-based downloads.")
return
if not message.input:
await response.edit("❌ <b>No torrent URL provided!</b>\n\n"
"<b>Usage:</b>\n"
"• <code>.torrent [torrent_url]</code>\n"
"• <code>.torrent -pd [torrent_url]</code> (upload to PixelDrain)\n"
"• <code>.torrent -pd -del [torrent_url]</code> (upload and delete local)")
return
torrent_url = message.filtered_input.strip()
upload_to_pixeldrain = "-pd" in message.flags
delete_after_upload = "-del" in message.flags
if upload_to_pixeldrain and not PIXELDRAIN_AVAILABLE:
await response.edit("❌ <b>PixelDrain module not available!</b>\n"
"Cannot upload to PixelDrain.")
return
try:
# Download torrent file
await response.edit("📥 <b>Downloading torrent file...</b>")
if torrent_url.startswith('magnet:'):
try:
torrent_data = parse_magnet_link(torrent_url)
except Exception as e:
await response.edit(f"❌ <b>Magnet link error:</b>\n<code>{str(e)}</code>")
return
else:
torrent_data = await download_torrent_file(torrent_url)
# Create download directory
torrent_hash = hashlib.sha1(torrent_data).hexdigest()[:10]
download_path = torrent_leecher.download_dir / f"torrent_{torrent_hash}_{int(time.time())}"
download_path.mkdir(parents=True, exist_ok=True)
# Add torrent to session
await response.edit(" <b>Adding torrent to session...</b>")
info_hash = torrent_leecher.add_torrent(torrent_data, download_path)
# Progress tracking function
async def progress_callback(status):
progress_text = f"📊 <b>Downloading Torrent</b>\n\n"
progress_text += f"📁 <b>Name:</b> <code>{status['name']}</code>\n"
progress_text += f"📈 <b>Progress:</b> {status['progress']:.1%}\n"
progress_text += f"⬇️ <b>Speed:</b> {status['download_rate'] / 1024 / 1024:.1f} MB/s\n"
progress_text += f"⬆️ <b>Upload:</b> {status['upload_rate'] / 1024 / 1024:.1f} MB/s\n"
progress_text += f"👥 <b>Peers:</b> {status['num_peers']} ({status['num_seeds']} seeds)\n"
progress_text += f"📊 <b>Size:</b> {status['downloaded'] / 1024 / 1024:.1f}/{status['total_size'] / 1024 / 1024:.1f} MB\n"
progress_text += f"⏱ <b>ETA:</b> {status['eta']}\n"
progress_text += f"🔄 <b>State:</b> {status['state']}"
try:
await response.edit(progress_text)
except:
pass # Ignore edit errors due to rate limiting
# Start download
await response.edit("🚀 <b>Starting torrent download...</b>")
downloaded_files = await torrent_leecher.download_torrent(info_hash, progress_callback)
if not downloaded_files:
await response.edit("❌ <b>No files were downloaded!</b>")
return
# Upload to PixelDrain if requested
uploaded_links = []
if upload_to_pixeldrain:
await response.edit("📤 <b>Uploading files to PixelDrain...</b>")
for file_path in downloaded_files:
try:
file_info = await pixeldrain.upload_file(file_path, response)
uploaded_links.append({
'name': file_path.name,
'url': file_info['url'],
'size': file_info['size']
})
# Delete local file if requested
if delete_after_upload and file_path.exists():
file_path.unlink()
except Exception as e:
uploaded_links.append({
'name': file_path.name,
'error': str(e)
})
# Clean up torrent from session
torrent_leecher.remove_torrent(info_hash, delete_files=delete_after_upload)
# Format final response
result_text = f"✅ <b>Torrent download completed!</b>\n\n"
torrent_status = torrent_leecher.get_torrent_status(info_hash)
if torrent_status:
result_text += f"📁 <b>Name:</b> <code>{torrent_status['name']}</code>\n"
result_text += f"📊 <b>Total Size:</b> {torrent_status['total_size'] / 1024 / 1024:.1f} MB\n"
result_text += f"📂 <b>Files Downloaded:</b> {len(downloaded_files)}\n\n"
if upload_to_pixeldrain:
result_text += f"🔗 <b>PixelDrain Links:</b>\n"
for link_info in uploaded_links:
if 'error' in link_info:
result_text += f"❌ <code>{link_info['name']}</code>: {link_info['error']}\n"
else:
size_mb = link_info['size'] / 1024 / 1024
result_text += f"✅ <code>{link_info['name']}</code> ({size_mb:.1f} MB)\n"
result_text += f" {link_info['url']}\n\n"
else:
result_text += f"📍 <b>Local Path:</b>\n<code>{download_path}</code>\n\n"
result_text += f"📋 <b>Files:</b>\n"
for file_path in downloaded_files[:10]: # Show max 10 files
size_mb = file_path.stat().st_size / 1024 / 1024
result_text += f"• <code>{file_path.name}</code> ({size_mb:.1f} MB)\n"
if len(downloaded_files) > 10:
result_text += f"... and {len(downloaded_files) - 10} more files"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Torrent download failed!</b>\n\n<code>{str(e)}</code>")
# Clean up on error
if 'info_hash' in locals():
torrent_leecher.remove_torrent(info_hash, delete_files=True)
@bot.add_cmd(cmd="torrentlist")
async def torrent_list(bot: BOT, message: Message):
"""
CMD: TORRENTLIST
INFO: List active torrents
USAGE: .torrentlist
"""
if not torrent_leecher.active_torrents:
await message.reply("📭 <b>No active torrents</b>")
return
list_text = f"📋 <b>Active Torrents ({len(torrent_leecher.active_torrents)})</b>\n\n"
for info_hash, torrent_data in torrent_leecher.active_torrents.items():
status = torrent_leecher.get_torrent_status(info_hash)
if status:
elapsed = int(time.time() - torrent_data['start_time'])
elapsed_str = f"{elapsed//3600}h {(elapsed%3600)//60}m" if elapsed > 3600 else f"{elapsed//60}m {elapsed%60}s"
list_text += f"🔸 <b>{status['name'][:30]}...</b>\n"
list_text += f" 📈 Progress: {status['progress']:.1%}\n"
list_text += f" ⬇️ Speed: {status['download_rate'] / 1024 / 1024:.1f} MB/s\n"
list_text += f" ⏱ Runtime: {elapsed_str}\n"
list_text += f" 🆔 Hash: <code>{info_hash[:12]}...</code>\n\n"
await message.reply(list_text)
@bot.add_cmd(cmd="torrentstop")
async def torrent_stop(bot: BOT, message: Message):
"""
CMD: TORRENTSTOP
INFO: Stop and remove a torrent
FLAGS: -del to delete downloaded files
USAGE:
.torrentstop [info_hash]
.torrentstop -del [info_hash]
"""
if not message.input:
await message.reply("❌ <b>No torrent hash provided!</b>\n\n"
"Use <code>.torrentlist</code> to see active torrents.")
return
info_hash = message.filtered_input.strip()
delete_files = "-del" in message.flags
if info_hash not in torrent_leecher.active_torrents:
await message.reply("❌ <b>Torrent not found!</b>\n\n"
"Use <code>.torrentlist</code> to see active torrents.")
return
try:
torrent_name = torrent_leecher.active_torrents[info_hash]['info'].name()
torrent_leecher.remove_torrent(info_hash, delete_files)
action = "stopped and files deleted" if delete_files else "stopped"
await message.reply(f"✅ <b>Torrent {action}!</b>\n\n"
f"📁 <b>Name:</b> <code>{torrent_name}</code>\n"
f"🆔 <b>Hash:</b> <code>{info_hash[:12]}...</code>")
except Exception as e:
await message.reply(f"❌ <b>Failed to stop torrent!</b>\n\n<code>{str(e)}</code>")
@bot.add_cmd(cmd="torrenthelp")
async def torrent_help(bot: BOT, message: Message):
"""
CMD: TORRENTHELP
INFO: Show torrent commands help
USAGE: .torrenthelp
"""
help_text = f"📋 <b>Torrent Leech Commands</b>\n\n"
help_text += f"🚀 <b>Download Commands:</b>\n"
help_text += f"• <code>.torrent [url]</code> - Download torrent\n"
help_text += f"• <code>.torrent -pd [url]</code> - Download and upload to PixelDrain\n"
help_text += f"• <code>.torrent -pd -del [url]</code> - Upload to PixelDrain and delete local\n\n"
help_text += f"📊 <b>Management Commands:</b>\n"
help_text += f"• <code>.torrentlist</code> - List active torrents\n"
help_text += f"• <code>.torrentstop [hash]</code> - Stop torrent\n"
help_text += f"• <code>.torrentstop -del [hash]</code> - Stop and delete files\n\n"
help_text += f"💡 <b>Usage Examples:</b>\n"
help_text += f"• <code>.torrent https://site.com/file.torrent</code>\n"
help_text += f"• <code>.torrent -pd https://site.com/movie.torrent</code>\n\n"
help_text += f"⚠️ <b>Notes:</b>\n"
help_text += f"• Only .torrent file URLs supported (no magnet links yet)\n"
help_text += f"• PixelDrain integration requires pixeldrain module\n"
help_text += f"• Downloaded files stored in downloads/torrents/\n"
help_text += f"• Use responsibly and respect copyright laws"
await message.reply(help_text)

203
app/plugins/files/upload.py Normal file
View File

@@ -0,0 +1,203 @@
import asyncio
import glob
import os
import time
from functools import partial
from typing import Union
from pyrogram.types import ReplyParameters
from ub_core.utils import (
Download,
DownloadedFile,
MediaType,
check_audio,
get_duration,
progress,
take_ss,
)
from app import BOT, Config, Message
UPLOAD_TYPES = Union[BOT.send_audio, BOT.send_document, BOT.send_photo, BOT.send_video]
async def video_upload(bot: BOT, file: DownloadedFile, has_spoiler: bool) -> UPLOAD_TYPES:
thumb = await take_ss(file.path, path=file.path)
if not await check_audio(file.path):
return partial(
bot.send_animation,
thumb=thumb,
unsave=True,
animation=file.path,
duration=await get_duration(file.path),
has_spoiler=has_spoiler,
)
return partial(
bot.send_video,
thumb=thumb,
video=file.path,
duration=await get_duration(file.path),
has_spoiler=has_spoiler,
)
async def photo_upload(bot: BOT, file: DownloadedFile, has_spoiler: bool) -> UPLOAD_TYPES:
return partial(bot.send_photo, photo=file.path, has_spoiler=has_spoiler)
async def audio_upload(bot: BOT, file: DownloadedFile, *_, **__) -> UPLOAD_TYPES:
return partial(bot.send_audio, audio=file.path, duration=await get_duration(file=file.path))
async def doc_upload(bot: BOT, file: DownloadedFile, *_, **__) -> UPLOAD_TYPES:
return partial(bot.send_document, document=file.path, disable_content_type_detection=True)
FILE_TYPE_MAP = {
MediaType.PHOTO: photo_upload,
MediaType.DOCUMENT: doc_upload,
MediaType.GIF: video_upload,
MediaType.AUDIO: audio_upload,
MediaType.VIDEO: video_upload,
}
def file_exists(file: str) -> bool:
return os.path.isfile(file)
def size_over_limit(size: int | float, client: BOT) -> bool:
limit = 3999 if client.me.is_premium else 1999
return size > limit
@BOT.add_cmd(cmd="upload")
async def upload(bot: BOT, message: Message):
"""
CMD: UPLOAD
INFO: Upload Media/Local Files/Plugins to TG.
FLAGS:
-d: to upload as doc.
-s: spoiler.
-bulk: for folder upload.
-r: file name regex [ to be used with -bulk only ]
USAGE:
.upload [-d] URL | Path to File | CMD
.upload -bulk downloads/videos
.upload -bulk -d -s downloads/videos
.upload -bulk -r -s downloads/videos/*.mp4 (only uploads mp4)
"""
input = message.filtered_input
if not input:
await message.reply("give a file url | path to upload.")
return
response = await message.reply("checking input...")
if input in Config.CMD_DICT:
await message.reply_document(document=Config.CMD_DICT[input].cmd_path)
await response.delete()
return
elif input.startswith("http") and not file_exists(input):
try:
async with Download(
url=input, dir=os.path.join("downloads", str(time.time())), message_to_edit=response
) as dl_obj:
if size_over_limit(dl_obj.size, client=bot):
await response.edit("<b>Aborted</b>, File size exceeds TG Limits!!!")
return
await response.edit("URL detected in input, Starting Download....")
file: DownloadedFile = await dl_obj.download()
except asyncio.exceptions.CancelledError:
await response.edit("Cancelled...")
return
except TimeoutError:
await response.edit("Download Timeout...")
return
except Exception as e:
await response.edit(str(e))
return
elif file_exists(input):
file = DownloadedFile(file=input)
if size_over_limit(file.size, client=bot):
await response.edit("<b>Aborted</b>, File size exceeds TG Limits!!!")
return
elif "-bulk" in message.flags:
await bulk_upload(message=message, response=response)
return
else:
await response.edit("invalid `cmd` | `url` | `file path`!!!")
return
await response.edit("Uploading....")
await upload_to_tg(file=file, message=message, response=response)
async def bulk_upload(message: Message, response: Message):
if "-r" in message.flags:
path_regex = message.filtered_input
else:
path_regex = os.path.join(message.filtered_input, "*")
file_list = [f for f in glob.glob(path_regex) if file_exists(f)]
if not file_list:
await response.edit("Invalid Folder path/regex or Folder Empty")
return
await response.edit(f"Preparing to upload {len(file_list)} files.")
for file in file_list:
file_info = DownloadedFile(file=file)
if size_over_limit(file_info.size, client=message._client):
await response.reply(f"Skipping {file_info.name} due to size exceeding limit.")
continue
temp_resp = await response.reply(f"starting to upload `{file_info.name}`")
await upload_to_tg(file=file_info, message=message, response=temp_resp)
await asyncio.sleep(3)
await response.delete()
async def upload_to_tg(file: DownloadedFile, message: Message, response: Message):
progress_args = (response, "Uploading...", file.path)
if "-d" in message.flags:
upload_method = partial(
message._client.send_document, document=file.path, disable_content_type_detection=True
)
else:
upload_method: UPLOAD_TYPES = await FILE_TYPE_MAP[file.type](
bot=message._client, file=file, has_spoiler="-s" in message.flags
)
try:
await upload_method(
chat_id=message.chat.id,
reply_parameters=ReplyParameters(message_id=message.reply_id),
progress=progress,
progress_args=progress_args,
caption=file.name,
)
await response.delete()
except asyncio.exceptions.CancelledError:
await response.edit("Cancelled....")
raise

90
app/plugins/misc/alive.py Normal file
View File

@@ -0,0 +1,90 @@
from sys import version_info
from pyrogram import __version__ as pyro_version
from pyrogram import filters
from pyrogram.raw.types.messages import BotResults
from pyrogram.types import (
InlineKeyboardButton,
InlineKeyboardMarkup,
InlineQuery,
InlineQueryResultAnimation,
InlineQueryResultPhoto,
ReplyParameters,
)
from ub_core.utils import MediaType, get_type
from ub_core.version import __version__ as core_version
from app import BOT, Config, Message, bot, extra_config
PY_VERSION = f"{version_info.major}.{version_info.minor}.{version_info.micro}"
@bot.add_cmd(cmd="alive")
async def alive(bot: BOT, message: Message):
# Inline Alive if Dual Mode
if bot.is_user and getattr(bot, "has_bot", False):
inline_result: BotResults = await bot.get_inline_bot_results(
bot=bot.bot.me.username, query="inline_alive"
)
await bot.send_inline_bot_result(
chat_id=message.chat.id,
result_id=inline_result.results[0].id,
query_id=inline_result.query_id,
)
return
kwargs = dict(
chat_id=message.chat.id,
caption=await get_alive_text(),
reply_markup=get_alive_buttons(client=bot),
reply_parameters=ReplyParameters(message_id=message.reply_id or message.id),
)
if get_type(url=extra_config.ALIVE_MEDIA) == MediaType.PHOTO:
await bot.send_photo(photo=extra_config.ALIVE_MEDIA, **kwargs)
else:
await bot.send_animation(animation=extra_config.ALIVE_MEDIA, unsave=True, **kwargs)
_bot = getattr(bot, "bot", bot)
if _bot.is_bot:
@_bot.on_inline_query(filters=filters.regex("^inline_alive$"), group=2)
async def return_inline_alive_results(client: BOT, inline_query: InlineQuery):
kwargs = dict(
title=f"Send Alive Media.",
caption=await get_alive_text(),
reply_markup=get_alive_buttons(client),
)
if get_type(url=extra_config.ALIVE_MEDIA) == MediaType.PHOTO:
result_type = InlineQueryResultPhoto(photo_url=extra_config.ALIVE_MEDIA, **kwargs)
else:
result_type = InlineQueryResultAnimation(
animation_url=extra_config.ALIVE_MEDIA, **kwargs
)
await inline_query.answer(results=[result_type], cache_time=300)
async def get_alive_text() -> str:
user_info = await bot.get_users(user_ids=Config.OWNER_ID)
return (
f"<b><a href='{Config.UPSTREAM_REPO}'>Plain-UB</a></b>, "
f"A simple Telegram User-Bot by @overspend1.\n"
f"\n User : <code>{user_info.first_name}</code>"
f"\n Python : <code>v{PY_VERSION}</code>"
f"\n Pyrogram : <code>v{pyro_version}</code>"
f"\n Core : <code>v{core_version}</code>"
)
def get_alive_buttons(client: BOT):
if not client.is_bot:
return
return InlineKeyboardMarkup(
[
[InlineKeyboardButton(text=f"UB-Core", url=Config.UPDATE_REPO)],
[InlineKeyboardButton(text=f"Support Group", url="t.me/plainub")],
]
)

View File

@@ -0,0 +1,13 @@
from ub_core.utils import run_shell_cmd
from app import BOT, Message
@BOT.add_cmd(cmd="extupdate", allow_sudo=False)
async def extra_modules_updater(bot: BOT, message: Message):
output = await run_shell_cmd(cmd="cd app/modules && git pull", timeout=10)
await message.reply(f"<pre language=shell>{output}</pre>")
if output.strip() != "Already up to date.":
bot.raise_sigint()

View File

@@ -0,0 +1,58 @@
from functools import wraps
from pyrogram.raw.types.messages import BotResults
from ub_core import BOT, Message
def run_with_timeout_guard(func):
@wraps(func)
async def inner(bot: BOT, message: Message):
try:
query_id, result_id, error = await func(bot, message)
if error:
await message.reply(error)
return
await bot.send_inline_bot_result(
chat_id=message.chat.id, query_id=query_id, result_id=result_id
)
except Exception as e:
await message.reply(str(e), del_in=10)
return inner
@BOT.add_cmd("ln")
@run_with_timeout_guard
async def last_fm_now(bot: BOT, message: Message):
"""
CMD: LN
INFO: Check LastFM Status
USAGE: .ln
"""
result: BotResults = await bot.get_inline_bot_results(bot="lastfmrobot")
if not result.results:
return None, None, "No results found."
return result.query_id, result.results[0].id, ""
@BOT.add_cmd("sn")
@run_with_timeout_guard
async def spotipie_now(bot: BOT, message: Message):
"""
CMD: SN
INFO: Check Spotipie Now
USAGE: .sn
"""
result: BotResults = await bot.get_inline_bot_results(bot="spotipiebot")
if not result.results:
return None, None, "No results found."
return result.query_id, result.results[0].id, ""

114
app/plugins/misc/song.py Normal file
View File

@@ -0,0 +1,114 @@
import asyncio
import json
import shutil
from pathlib import Path
from time import time
from urllib.parse import urlparse
from pyrogram.enums import MessageEntityType
from pyrogram.types import InputMediaAudio
from ub_core.utils import aio, run_shell_cmd
from app import BOT, Message
domains = [
"www.youtube.com",
"youtube.com",
"m.youtube.com",
"youtu.be",
"www.youtube-nocookie.com",
"music.youtube.com",
]
def is_yt_url(url: str) -> bool:
return urlparse(url).netloc in domains
def extract_link_from_reply(message: Message) -> str | None:
if not message:
return
for link in message.text_list:
if is_yt_url(link):
return link
for entity in message.entities or []:
if entity.type == MessageEntityType.TEXT_LINK and is_yt_url(entity.url):
return entity.url
return None
@BOT.add_cmd(cmd="song")
async def song_dl(bot: BOT, message: Message) -> None | Message:
query = extract_link_from_reply(message.replied) or message.filtered_input
if not query:
await message.reply("Give a song name or link to download.")
return
response: Message = await message.reply("Searching....")
download_path: str = Path("downloads") / str(time())
query_or_search: str = query if query.startswith("http") else f"ytsearch:{query}"
song_info: dict = await get_download_info(query=query_or_search, path=download_path)
audio_files: list = list(download_path.glob("*mp3"))
if not audio_files:
await response.edit("Song Not found.")
return
audio_file = audio_files[0]
url = song_info.get("webpage_url")
await response.edit(f"`Uploading {audio_file.name}....`")
await response.edit_media(
InputMediaAudio(
media=str(audio_file),
caption=f"<a href={url}>{audio_file.name}</a>" if url else None,
duration=int(song_info.get("duration", 0)),
performer=song_info.get("channel", ""),
thumb=await aio.in_memory_dl(song_info.get("thumbnail")),
)
)
shutil.rmtree(download_path, ignore_errors=True)
async def get_download_info(query: str, path: Path) -> dict:
download_cmd = (
f"yt-dlp -o '{path/'%(title)s.%(ext)s'}' "
f"-f 'bestaudio' "
f"--no-warnings "
f"--ignore-errors "
f"--ignore-no-formats-error "
f"--quiet "
f"--no-playlist "
f"--audio-quality 0 "
f"--audio-format mp3 "
f"--extract-audio "
f"--embed-thumbnail "
f"--embed-metadata "
f"--print-json "
f"'{query}'"
)
try:
song_info = (await run_shell_cmd(download_cmd, timeout=60, ret_val="")).strip()
serialised_json = json.loads(song_info)
return serialised_json
except asyncio.TimeoutError:
shutil.rmtree(path=path, ignore_errors=True)
except json.JSONDecodeError:
pass
return {}

View File

@@ -0,0 +1,420 @@
import asyncio
import socket
import subprocess
import re
from datetime import datetime
from app import BOT, Message
@BOT.add_cmd(cmd="ping")
async def enhanced_ping(bot: BOT, message: Message):
"""
CMD: PING
INFO: Enhanced ping command with detailed statistics.
FLAGS: -c [count] for number of pings (default: 4)
USAGE: .ping google.com
.ping -c 10 8.8.8.8
"""
target = message.filtered_input
if not target:
await message.reply("❌ <b>No target provided!</b>\n"
"Usage: <code>.ping [hostname/ip]</code>\n"
"Example: <code>.ping google.com</code>")
return
# Parse count flag
count = 4 # Default count
if "-c" in message.flags:
try:
count_index = message.flags.index("-c") + 1
if count_index < len(message.flags):
count = min(int(message.flags[count_index]), 20) # Max 20 pings
except (ValueError, IndexError):
pass
response = await message.reply(f"🔄 <b>Pinging {target}...</b>\n<i>Sending {count} packets...</i>")
try:
# Run ping command
if count == 1:
cmd = ['ping', '-c', '1', target]
else:
cmd = ['ping', '-c', str(count), target]
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode().strip()
await response.edit(f"❌ <b>Ping failed!</b>\n"
f"<b>Target:</b> <code>{target}</code>\n"
f"<b>Error:</b> <code>{error_msg}</code>")
return
output = stdout.decode().strip()
# Parse ping results
lines = output.split('\n')
# Extract statistics
stats_line = next((line for line in lines if 'transmitted' in line), '')
time_line = next((line for line in lines if 'min/avg/max' in line), '')
# Parse individual ping times
ping_times = []
for line in lines:
if 'time=' in line:
time_match = re.search(r'time=([0-9.]+)', line)
if time_match:
ping_times.append(float(time_match.group(1)))
# Build result
ping_text = f"🏓 <b>Ping Results</b>\n\n"
ping_text += f"<b>Target:</b> <code>{target}</code>\n"
ping_text += f"<b>Packets:</b> {count}\n"
if stats_line:
ping_text += f"<b>Statistics:</b> <code>{stats_line}</code>\n"
if time_line:
ping_text += f"<b>Timing:</b> <code>{time_line}</code>\n"
if ping_times:
avg_time = sum(ping_times) / len(ping_times)
min_time = min(ping_times)
max_time = max(ping_times)
ping_text += f"\n<b>📊 Detailed Analysis:</b>\n"
ping_text += f"<b>Average:</b> {avg_time:.2f} ms\n"
ping_text += f"<b>Minimum:</b> {min_time:.2f} ms\n"
ping_text += f"<b>Maximum:</b> {max_time:.2f} ms\n"
# Simple quality assessment
if avg_time < 50:
quality = "🟢 Excellent"
elif avg_time < 100:
quality = "🟡 Good"
elif avg_time < 200:
quality = "🟠 Fair"
else:
quality = "🔴 Poor"
ping_text += f"<b>Quality:</b> {quality}"
ping_text += f"\n\n✅ <b>Ping completed!</b>"
await response.edit(ping_text)
except Exception as e:
await response.edit(f"❌ <b>Ping error:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="whois")
async def whois_lookup(bot: BOT, message: Message):
"""
CMD: WHOIS
INFO: Perform WHOIS lookup for domains and IP addresses.
USAGE: .whois google.com
.whois 8.8.8.8
"""
target = message.filtered_input
if not target:
await message.reply("❌ <b>No domain/IP provided!</b>\n"
"Usage: <code>.whois [domain/ip]</code>\n"
"Example: <code>.whois google.com</code>")
return
response = await message.reply(f"🔍 <b>Looking up WHOIS for {target}...</b>")
try:
# Run whois command
process = await asyncio.create_subprocess_exec(
'whois', target,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode().strip()
await response.edit(f"❌ <b>WHOIS lookup failed!</b>\n"
f"<b>Target:</b> <code>{target}</code>\n"
f"<b>Error:</b> <code>{error_msg}</code>")
return
output = stdout.decode().strip()
# Parse important information
lines = output.split('\n')
# Extract key information
registrar = ""
creation_date = ""
expiration_date = ""
name_servers = []
for line in lines:
line = line.strip()
if 'Registrar:' in line:
registrar = line.split(':', 1)[1].strip()
elif 'Creation Date:' in line or 'Created:' in line:
creation_date = line.split(':', 1)[1].strip()
elif 'Expiration Date:' in line or 'Expires:' in line:
expiration_date = line.split(':', 1)[1].strip()
elif 'Name Server:' in line:
ns = line.split(':', 1)[1].strip().lower()
if ns not in name_servers:
name_servers.append(ns)
# Truncate output if too long
if len(output) > 3000:
output = output[:3000] + "\n... (output truncated)"
whois_text = f"🔍 <b>WHOIS Lookup Results</b>\n\n"
whois_text += f"<b>Domain/IP:</b> <code>{target}</code>\n"
if registrar:
whois_text += f"<b>Registrar:</b> <code>{registrar}</code>\n"
if creation_date:
whois_text += f"<b>Created:</b> <code>{creation_date}</code>\n"
if expiration_date:
whois_text += f"<b>Expires:</b> <code>{expiration_date}</code>\n"
if name_servers:
whois_text += f"<b>Name Servers:</b>\n"
for ns in name_servers[:5]: # Show max 5 name servers
whois_text += f" • <code>{ns}</code>\n"
whois_text += f"\n<b>📋 Full WHOIS Data:</b>\n<pre>{output}</pre>"
await response.edit(whois_text)
except Exception as e:
await response.edit(f"❌ <b>WHOIS error:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="nslookup")
async def dns_lookup(bot: BOT, message: Message):
"""
CMD: NSLOOKUP
INFO: Perform DNS lookup for domains.
FLAGS: -type [A/AAAA/MX/NS/TXT] for specific record types
USAGE: .nslookup google.com
.nslookup -type MX gmail.com
"""
target = message.filtered_input
if not target:
await message.reply("❌ <b>No domain provided!</b>\n"
"Usage: <code>.nslookup [domain]</code>\n"
"Example: <code>.nslookup google.com</code>")
return
# Parse record type
record_type = "A" # Default
if "-type" in message.flags:
try:
type_index = message.flags.index("-type") + 1
if type_index < len(message.flags):
record_type = message.flags[type_index].upper()
except (ValueError, IndexError):
pass
response = await message.reply(f"🔍 <b>DNS lookup for {target} ({record_type})...</b>")
try:
# Run nslookup command
process = await asyncio.create_subprocess_exec(
'nslookup', '-type=' + record_type, target,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
output = stdout.decode().strip()
if process.returncode != 0 or "can't find" in output.lower():
await response.edit(f"❌ <b>DNS lookup failed!</b>\n"
f"<b>Domain:</b> <code>{target}</code>\n"
f"<b>Type:</b> <code>{record_type}</code>\n"
f"<b>Output:</b> <pre>{output}</pre>")
return
# Parse results for cleaner display
lines = output.split('\n')
# Extract relevant information
results = []
in_answer_section = False
for line in lines:
line = line.strip()
if 'Non-authoritative answer:' in line:
in_answer_section = True
continue
elif in_answer_section and line:
if not line.startswith('Name:') and not line.startswith('Address:'):
results.append(line)
dns_text = f"🔍 <b>DNS Lookup Results</b>\n\n"
dns_text += f"<b>Domain:</b> <code>{target}</code>\n"
dns_text += f"<b>Record Type:</b> <code>{record_type}</code>\n\n"
if results:
dns_text += f"<b>📋 Results:</b>\n"
for result in results[:10]: # Limit to 10 results
dns_text += f"<code>{result}</code>\n"
dns_text += f"\n<b>📋 Full Output:</b>\n<pre>{output}</pre>"
await response.edit(dns_text)
except Exception as e:
await response.edit(f"❌ <b>DNS lookup error:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="ipinfo")
async def ip_info(bot: BOT, message: Message):
"""
CMD: IPINFO
INFO: Get information about an IP address or domain.
USAGE: .ipinfo 8.8.8.8
.ipinfo google.com
"""
target = message.filtered_input
if not target:
await message.reply("❌ <b>No IP/domain provided!</b>\n"
"Usage: <code>.ipinfo [ip/domain]</code>\n"
"Example: <code>.ipinfo 8.8.8.8</code>")
return
response = await message.reply(f"🔍 <b>Getting IP information for {target}...</b>")
try:
# First, resolve domain to IP if needed
try:
ip_address = socket.gethostbyname(target)
except socket.gaierror:
# Assume it's already an IP
ip_address = target
# Validate IP address
try:
socket.inet_aton(ip_address)
except socket.error:
await response.edit(f"❌ <b>Invalid IP address or domain!</b>\n"
f"<b>Target:</b> <code>{target}</code>")
return
# Get basic IP information
info_text = f"🌐 <b>IP Address Information</b>\n\n"
info_text += f"<b>Target:</b> <code>{target}</code>\n"
info_text += f"<b>IP Address:</b> <code>{ip_address}</code>\n"
# Check if it's a private IP
ip_parts = ip_address.split('.')
if len(ip_parts) == 4:
first_octet = int(ip_parts[0])
second_octet = int(ip_parts[1])
if (first_octet == 10 or
(first_octet == 172 and 16 <= second_octet <= 31) or
(first_octet == 192 and second_octet == 168) or
first_octet == 127):
info_text += f"<b>Type:</b> 🏠 Private/Local IP\n"
else:
info_text += f"<b>Type:</b> 🌍 Public IP\n"
# Try to get hostname if different from input
if target != ip_address:
try:
hostname = socket.gethostbyaddr(ip_address)[0]
info_text += f"<b>Hostname:</b> <code>{hostname}</code>\n"
except socket.herror:
pass
# Try reverse DNS lookup
try:
reverse_dns = socket.gethostbyaddr(ip_address)[0]
if reverse_dns != target:
info_text += f"<b>Reverse DNS:</b> <code>{reverse_dns}</code>\n"
except socket.herror:
info_text += f"<b>Reverse DNS:</b> <i>Not available</i>\n"
info_text += f"\n✅ <b>IP information retrieved!</b>"
await response.edit(info_text)
except Exception as e:
await response.edit(f"❌ <b>IP info error:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="traceroute")
async def trace_route(bot: BOT, message: Message):
"""
CMD: TRACEROUTE
INFO: Trace the route to a destination.
USAGE: .traceroute google.com
"""
target = message.filtered_input
if not target:
await message.reply("❌ <b>No target provided!</b>\n"
"Usage: <code>.traceroute [hostname/ip]</code>\n"
"Example: <code>.traceroute google.com</code>")
return
response = await message.reply(f"🔄 <b>Tracing route to {target}...</b>\n<i>This may take a moment...</i>")
try:
# Run traceroute command (use traceroute on Linux/Mac, tracert on Windows)
try:
# Try traceroute first (Linux/Mac)
process = await asyncio.create_subprocess_exec(
'traceroute', '-m', '15', target, # Max 15 hops
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
except FileNotFoundError:
# Try tracert (Windows)
process = await asyncio.create_subprocess_exec(
'tracert', '-h', '15', target, # Max 15 hops
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
try:
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=60.0)
except asyncio.TimeoutError:
process.kill()
await response.edit("⏰ <b>Traceroute timed out after 60 seconds!</b>")
return
if process.returncode != 0:
error_msg = stderr.decode().strip()
await response.edit(f"❌ <b>Traceroute failed!</b>\n"
f"<b>Target:</b> <code>{target}</code>\n"
f"<b>Error:</b> <code>{error_msg}</code>")
return
output = stdout.decode().strip()
# Truncate if too long
if len(output) > 3500:
output = output[:3500] + "\n... (output truncated)"
trace_text = f"🗺️ <b>Traceroute Results</b>\n\n"
trace_text += f"<b>Target:</b> <code>{target}</code>\n"
trace_text += f"<b>Max Hops:</b> 15\n\n"
trace_text += f"<b>📋 Route Trace:</b>\n<pre>{output}</pre>"
trace_text += f"\n✅ <b>Traceroute completed!</b>"
await response.edit(trace_text)
except Exception as e:
await response.edit(f"❌ <b>Traceroute error:</b>\n<code>{str(e)}</code>")

View File

@@ -0,0 +1,118 @@
from app import BOT, Config, CustomDB, Message
DB = CustomDB["SUDO_CMD_LIST"]
async def init_task():
async for sudo_cmd in DB.find():
cmd_object = Config.CMD_DICT.get(sudo_cmd["_id"])
if cmd_object:
cmd_object.loaded = True
@BOT.add_cmd(cmd="addscmd", allow_sudo=False)
async def add_scmd(bot: BOT, message: Message):
"""
CMD: ADDSCMD
INFO: Add Sudo Commands.
FLAGS: -all to instantly add all Commands.
USAGE:
.addscmd ping | .addscmd -all
"""
if "-all" in message.flags:
cmds = []
for cmd_name, cmd_object in Config.CMD_DICT.items():
if cmd_object.sudo:
cmd_object.loaded = True
cmds.append({"_id": cmd_name})
await DB.drop()
await DB.insert_many(cmds)
await (await message.reply("All Commands Added to Sudo!")).log()
return
cmd_name = message.filtered_input
cmd_object = Config.CMD_DICT.get(cmd_name)
response = await message.reply(f"Adding <b>{cmd_name}</b> to sudo....")
if not cmd_object:
await response.edit(text=f"<b>{cmd_name}</b> not a valid command.", del_in=10)
return
elif not cmd_object.sudo:
await response.edit(text=f"<b>{cmd_name}</b> is disabled for sudo users.", del_in=10)
return
elif cmd_object.loaded:
await response.edit(text=f"<b>{cmd_name}</b> already in Sudo!", del_in=10)
return
resp_str = f"#SUDO\n<b>{cmd_name}</b> added to Sudo!"
if "-temp" in message.flags:
resp_str += "\nTemp: True"
else:
await DB.add_data(data={"_id": cmd_name})
cmd_object.loaded = True
await (await response.edit(resp_str)).log()
@BOT.add_cmd(cmd="delscmd", allow_sudo=False)
async def del_scmd(bot: BOT, message: Message):
"""
CMD: DELSCMD
INFO: Remove Sudo Commands.
FLAGS: -all to instantly remove all Commands.
USAGE:
.delscmd ping | .delscmd -all
"""
if "-all" in message.flags:
for cmd_object in Config.CMD_DICT.values():
cmd_object.loaded = False
await DB.drop()
await (await message.reply("All Commands Removed from Sudo!")).log()
return
cmd_name = message.filtered_input
cmd_object = Config.CMD_DICT.get(cmd_name)
if not cmd_object:
return
response = await message.reply(f"Removing <b>{cmd_name}</b> from sudo....")
if not cmd_object.loaded:
await response.edit(f"<b>{cmd_name}</b> not in Sudo!")
return
cmd_object.loaded = False
resp_str = f"#SUDO\n<b>{cmd_name}</b> removed from Sudo!"
if "-temp" in message.flags:
resp_str += "\nTemp: True"
else:
await DB.delete_data(cmd_name)
await (await response.edit(resp_str)).log()
@BOT.add_cmd(cmd="vscmd")
async def view_sudo_cmd(bot: BOT, message: Message):
cmds = [cmd_name for cmd_name, cmd_obj in Config.CMD_DICT.items() if cmd_obj.loaded]
if not cmds:
await message.reply("No Commands in SUDO!")
return
await message.reply(
text=f"List of <b>{len(cmds)}</b>:\n <pre language=json>{cmds}</pre>",
del_in=30,
block=False,
)

View File

@@ -0,0 +1,38 @@
from pyrogram import filters
from app import BOT, Config, Message, bot
from app.plugins.sudo.users import SUDO_USERS
@BOT.add_cmd(cmd="disable_su", allow_sudo=False)
async def disable_su(bot: BOT, message: Message):
u_id = message.from_user.id
if u_id in Config.DISABLED_SUPERUSERS:
return
Config.DISABLED_SUPERUSERS.append(u_id)
await SUDO_USERS.add_data({"_id": u_id, "disabled": True})
await message.reply(
text="Your <b>SuperUser</b> Access is now <code>Disabled</code>.", del_in=10
)
@bot.on_message(
filters=filters.command(commands="enable_su", prefixes=Config.SUDO_TRIGGER)
& filters.create(lambda _, __, m: m.from_user and m.from_user.id in Config.DISABLED_SUPERUSERS),
group=1,
is_command=True,
filters_edited=True,
check_for_reactions=True,
)
async def enable_su(bot: BOT, message: Message):
u_id = message.from_user.id
Config.DISABLED_SUPERUSERS.remove(u_id)
await SUDO_USERS.add_data({"_id": u_id, "disabled": False})
await message.reply(text="Your <b>SuperUser</b> Access is now <code>Enabled</code>.", del_in=10)

187
app/plugins/sudo/users.py Normal file
View File

@@ -0,0 +1,187 @@
from pyrogram.types import User
from ub_core.utils.helpers import extract_user_data, get_name
from app import BOT, Config, CustomDB, Message
SUDO = CustomDB["COMMON_SETTINGS"]
SUDO_USERS = CustomDB["SUDO_USERS"]
async def init_task():
sudo = await SUDO.find_one({"_id": "sudo_switch"}) or {}
Config.SUDO = sudo.get("value", False)
async for sudo_user in SUDO_USERS.find():
config = Config.SUPERUSERS if sudo_user.get("super") else Config.SUDO_USERS
config.append(sudo_user["_id"])
if sudo_user.get("disabled"):
Config.DISABLED_SUPERUSERS.append(sudo_user["_id"])
@BOT.add_cmd(cmd="sudo", allow_sudo=False)
async def sudo(bot: BOT, message: Message):
"""
CMD: SUDO
INFO: Enable/Disable sudo..
FLAGS: -c to check sudo status.
USAGE:
.sudo | .sudo -c
"""
if "-c" in message.flags:
await message.reply(text=f"Sudo is enabled: <b>{Config.SUDO}</b>!", del_in=8)
return
value = not Config.SUDO
Config.SUDO = value
await SUDO.add_data({"_id": "sudo_switch", "value": value})
await (await message.reply(text=f"Sudo is enabled: <b>{value}</b>!", del_in=8)).log()
@BOT.add_cmd(cmd="addsudo", allow_sudo=False)
async def add_sudo(bot: BOT, message: Message) -> Message | None:
"""
CMD: ADDSUDO
INFO: Add Sudo User.
FLAGS:
-temp: to temporarily add until bot restarts.
-su: to give SuperUser access.
USAGE:
.addsudo [-temp | -su] [ UID | @ | Reply to Message ]
"""
response = await message.reply("Extracting User info...")
user, _ = await message.extract_user_n_reason()
if not isinstance(user, User):
await response.edit("unable to extract user info.")
return
if "-su" in message.flags:
add_list, remove_list = Config.SUPERUSERS, Config.SUDO_USERS
text = "Super Users"
else:
add_list, remove_list = Config.SUDO_USERS, Config.SUPERUSERS
text = "Sudo Users"
if user.id in add_list:
await response.edit(
text=f"{get_name(user)} already in Sudo with same privileges!", del_in=5
)
return
response_str = f"#SUDO\n{user.mention} added to {text} List."
add_and_remove(user.id, add_list, remove_list)
if "-temp" not in message.flags:
await SUDO_USERS.add_data(
{
"_id": user.id,
**extract_user_data(user),
"disabled": False,
"super": "-su" in message.flags,
}
)
else:
response_str += "\n<b>Temporary</b>: True"
await response.edit(text=response_str, del_in=5)
await response.log()
@BOT.add_cmd(cmd="delsudo", allow_sudo=False)
async def remove_sudo(bot: BOT, message: Message) -> Message | None:
"""
CMD: DELSUDO
INFO: Add Remove User.
FLAGS:
-temp: to temporarily remove until bot restarts.
-su: to Remove SuperUser Access.
-f: force rm an id
USAGE:
.delsudo [-temp] [ UID | @ | Reply to Message ]
"""
if "-f" in message.flags:
await SUDO_USERS.delete_data(id=int(message.filtered_input))
await message.reply(f"Forcefully deleted {message.filtered_input} from sudo users.")
return
response = await message.reply("Extracting User info...")
user, _ = await message.extract_user_n_reason()
if isinstance(user, str):
await response.edit(user)
return
if not isinstance(user, User):
await response.edit("unable to extract user info.")
return
if user.id not in {*Config.SUDO_USERS, *Config.SUPERUSERS}:
await response.edit(text=f"{get_name(user)} not in Sudo!", del_in=5)
return
if "-su" in message.flags:
response_str = f"{user.mention}'s Super User access is revoked to Sudo only."
add_and_remove(user.id, Config.SUDO_USERS, Config.SUPERUSERS)
else:
add_and_remove(user.id, remove_list=Config.SUPERUSERS)
add_and_remove(user.id, remove_list=Config.SUDO_USERS)
response_str = f"{user.mention}'s access to bot has been removed."
if "-temp" not in message.flags:
if "-su" in message.flags:
await SUDO_USERS.add_data({"_id": user.id, "super": False})
else:
await SUDO_USERS.delete_data(id=user.id)
else:
response_str += "\n<b>Temporary</b>: True"
await response.edit(text=response_str, del_in=5)
await response.log()
def add_and_remove(u_id: int, add_list: list | None = None, remove_list: list | None = None):
if add_list is not None and u_id not in add_list:
add_list.append(u_id)
if remove_list is not None and u_id in remove_list:
remove_list.remove(u_id)
@BOT.add_cmd(cmd="vsudo")
async def sudo_list(bot: BOT, message: Message):
"""
CMD: VSUDO
INFO: View Sudo Users.
FLAGS: -id to get UIDs
USAGE:
.vsudo | .vsudo -id
"""
output: str = ""
total = 0
async for user in SUDO_USERS.find():
output += f'\n<b>• {user["name"]}</b>'
if "-id" in message.flags:
output += f'\n ID: <code>{user["_id"]}</code>'
output += f'\n Super: <b>{user.get("super", False)}</b>'
output += f'\n Disabled: <b>{user.get("disabled", False)}</b>\n'
total += 1
if not total:
await message.reply("You don't have any SUDO USERS.")
return
output: str = f"List of <b>{total}</b> SUDO USERS:\n{output}"
await message.reply(output, del_in=30, block=True)

View File

@@ -0,0 +1,246 @@
import platform
import psutil
from datetime import datetime
from app import BOT, Message
@BOT.add_cmd(cmd="neofetch")
async def neofetch_info(bot: BOT, message: Message):
"""
CMD: NEOFETCH
INFO: Display system information in neofetch style with ASCII art.
USAGE: .neofetch
"""
response = await message.reply("🔄 <b>Generating neofetch output...</b>")
try:
# Get system information
system = platform.system()
distro = platform.platform()
hostname = platform.node()
kernel = platform.release()
architecture = platform.machine()
# CPU info
cpu_info = platform.processor() or "Unknown"
cpu_cores = psutil.cpu_count(logical=True)
cpu_usage = psutil.cpu_percent(interval=1)
# Memory info
memory = psutil.virtual_memory()
memory_used_gb = memory.used / (1024**3)
memory_total_gb = memory.total / (1024**3)
# Uptime
boot_time = psutil.boot_time()
uptime = datetime.now() - datetime.fromtimestamp(boot_time)
uptime_str = str(uptime).split('.')[0]
# Choose ASCII art based on OS
if "Windows" in system:
ascii_art = """🪟 Windows
██╗ ██╗██╗███╗ ██╗
██║ ██║██║████╗ ██║
██║ █╗ ██║██║██╔██╗ ██║
██║███╗██║██║██║╚██╗██║
╚███╔███╔╝██║██║ ╚████║
╚══╝╚══╝ ╚═╝╚═╝ ╚═══╝"""
elif "Darwin" in system:
ascii_art = """🍎 macOS
'c.
,xNMM.
.OMMMMo
OMMM0,
.;loddo:' loolloddol;.
cKMMMMMMMMMMNWMMMMMMMMMM0:
.KMMMMMMMMMMMMMMMMMMMMMMMWd.
XMMMMMMMMMMMMMMMMMMMMMMMX.
;MMMMMMMMMMMMMMMMMMMMMMMM:
:MMMMMMMMMMMMMMMMMMMMMMMM:
.MMMMMMMMMMMMMMMMMMMMMMMMX.
kMMMMMMMMMMMMMMMMMMMMMMMMWd.
.XMMMMMMMMMMMMMMMMMMMMMMMMMMk
.XMMMMMMMMMMMMMMMMMMMMMMMMK.
kMMMMMMMMMMMMMMMMMMMMMMd
;KMMMMMMMWXXWMMMMMMMk.
.cooc,. .,coo:."""
else: # Linux/Unix
ascii_art = """🐧 Linux
#####
#######
##O#O##
#######
###########
#############
###############
################
#################
#####################
#####################
#################"""
# Create the neofetch-style output
info_lines = [
f"OS: {distro}",
f"Host: {hostname}",
f"Kernel: {kernel}",
f"Uptime: {uptime_str}",
f"CPU: {cpu_info} ({cpu_cores} cores)",
f"CPU Usage: {cpu_usage}%",
f"Memory: {memory_used_gb:.1f}GB / {memory_total_gb:.1f}GB ({memory.percent}%)",
f"Architecture: {architecture}",
]
# Combine ASCII art with info
art_lines = ascii_art.strip().split('\n')
max_art_width = max(len(line) for line in art_lines)
# Pad art lines and combine with info
output_lines = []
for i in range(max(len(art_lines), len(info_lines))):
art_part = art_lines[i] if i < len(art_lines) else " " * max_art_width
info_part = info_lines[i] if i < len(info_lines) else ""
output_lines.append(f"{art_part} {info_part}")
neofetch_output = f"<pre>{chr(10).join(output_lines)}</pre>"
await response.edit(neofetch_output)
except Exception as e:
await response.edit(f"❌ <b>Error generating neofetch:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="fetch")
async def minimal_fetch(bot: BOT, message: Message):
"""
CMD: FETCH
INFO: Display minimal system information.
USAGE: .fetch
"""
response = await message.reply("🔄 <b>Getting system info...</b>")
try:
# Get basic info
system = platform.system()
release = platform.release()
hostname = platform.node()
cpu_cores = psutil.cpu_count(logical=True)
# Memory
memory = psutil.virtual_memory()
memory_gb = memory.total / (1024**3)
# Uptime
boot_time = psutil.boot_time()
uptime = datetime.now() - datetime.fromtimestamp(boot_time)
days = uptime.days
hours, remainder = divmod(uptime.seconds, 3600)
minutes, _ = divmod(remainder, 60)
# Simple system emoji
if "Windows" in system:
emoji = "🪟"
elif "Darwin" in system:
emoji = "🍎"
else:
emoji = "🐧"
fetch_text = f"""{emoji} <b>{hostname}</b>
━━━━━━━━━━━━━━━━━━━━━
<b>OS:</b> {system} {release}
<b>CPU:</b> {cpu_cores} cores
<b>Memory:</b> {memory_gb:.1f} GB
<b>Uptime:</b> {days}d {hours}h {minutes}m"""
await response.edit(fetch_text)
except Exception as e:
await response.edit(f"❌ <b>Error:</b> {str(e)}")
@BOT.add_cmd(cmd="logo")
async def system_logo(bot: BOT, message: Message):
"""
CMD: LOGO
INFO: Display ASCII logo based on the operating system.
USAGE: .logo
"""
response = await message.reply("🔄 <b>Generating system logo...</b>")
try:
system = platform.system()
if "Windows" in system:
logo = """<pre>
██████╗ ██████╗ ███████╗████████╗████████╗██╗ ██╗ ██╗ ██╗██████╗
██╔══██╗██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝╚██╗ ██╔╝ ██║ ██║██╔══██╗
██████╔╝██████╔╝█████╗ ██║ ██║ ╚████╔╝ ██║ ██║██████╔╝
██╔═══╝ ██╔══██╗██╔══╝ ██║ ██║ ╚██╔╝ ██║ ██║██╔══██╗
██║ ██║ ██║███████╗ ██║ ██║ ██║ ╚██████╔╝██████╔╝
╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝
🪟 Windows System 🪟
</pre>"""
elif "Darwin" in system:
logo = """<pre>
████████
████████████████
██████████████████████
████████████████████████████
██████████████████████████████████
████████████████████████████████████████
████████████████ ████████████████████████
████████████████ ████████████████████████
████████████████ ████████████████████████
████████████████ ████████████████████████
████████████████ ████████████████████████
████████████████ ████████████████████
████████████████ ████████████████
████████████████ ████████████████
████████████████ ████████████████
████████████████ ████████████████
████████████████ ████████████████
████████████████ ████████████████
████████████████ ████████████████
🍎 macOS System 🍎
</pre>"""
else: # Linux
logo = """<pre>
.-/+oossssoo+/-.
`:+ssssssssssssssssss+:`
-+ssssssssssssssssssyyssss+-
.ossssssssssssssssss+. .+sssso.
/sssssssssss+++++++/ /ssssso
+sssssssss+. .sssssso
+sssssss+. +ssssss
+sssss+. +sssss
+ssss/ sssss-
.sss+ +sss.
`ss+ .-. .ss`
`ss. .ossso- ss`
.ss `ssssss+ ss.
+ss `ssssss+ ss+
+ss `ssssss+ ss+
.ss `ssssss+ ss.
`ss. .ossso- ss`
`ss+ .-. .ss`
.sss+ +sss.
+ssss/ sssss-
+sssss+. +sssss
+sssssss+. +ssssss
+sssssssss+. .sssssso
/sssssssssss+++++++/ /ssssso
.ossssssssssssssssss+. .+sssso.
-+ssssssssssssssssssyyssss+-
`:+ssssssssssssssssss+:`
.-/+oossssoo+/-.
🐧 Linux System 🐧
</pre>"""
await response.edit(logo)
except Exception as e:
await response.edit(f"❌ <b>Error:</b> {str(e)}")

232
app/plugins/system/shell.py Normal file
View File

@@ -0,0 +1,232 @@
import asyncio
import os
import shlex
from datetime import datetime
from app import BOT, Config, Message
# Dangerous commands that should be blocked
DANGEROUS_COMMANDS = [
'rm', 'sudo rm', 'del', 'format', 'fdisk', 'mkfs',
'dd', 'sudo dd', 'chmod 000', 'sudo chmod',
':(){ :|:& };:', 'shutdown', 'reboot', 'halt',
'sudo shutdown', 'sudo reboot', 'sudo halt',
'passwd', 'sudo passwd', 'userdel', 'sudo userdel'
]
def is_dangerous_command(command: str) -> bool:
"""Check if command contains dangerous operations"""
command_lower = command.lower().strip()
for dangerous in DANGEROUS_COMMANDS:
if command_lower.startswith(dangerous):
return True
return False
@BOT.add_cmd(cmd="sh", allow_sudo=False)
async def shell_command(bot: BOT, message: Message):
"""
CMD: SH
INFO: Execute shell commands safely (Owner only).
FLAGS: -o for output only, -t for with timestamps
USAGE: .sh ls -la
"""
# Only allow owner to use shell commands
if message.from_user.id != Config.OWNER_ID:
await message.reply("❌ <b>Access Denied:</b> Only the bot owner can execute shell commands.")
return
command = message.filtered_input
if not command:
await message.reply("❌ <b>No command provided.</b>\nUsage: <code>.sh [command]</code>")
return
# Check for dangerous commands
if is_dangerous_command(command):
await message.reply("⚠️ <b>Dangerous command blocked for safety!</b>\n"
f"Command: <code>{command}</code>\n\n"
"This command could harm your system.")
return
response = await message.reply(f"🔄 <b>Executing:</b> <code>{command}</code>")
try:
start_time = datetime.now()
# Execute command with timeout
process = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=os.getcwd()
)
# Wait for completion with timeout
try:
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=30.0)
except asyncio.TimeoutError:
process.kill()
await response.edit("⏰ <b>Command timed out after 30 seconds!</b>")
return
end_time = datetime.now()
execution_time = (end_time - start_time).total_seconds()
# Decode output
stdout_text = stdout.decode('utf-8', errors='ignore').strip()
stderr_text = stderr.decode('utf-8', errors='ignore').strip()
# Prepare output
result_text = f"💻 <b>Shell Command Result</b>\n\n"
result_text += f"<b>Command:</b> <code>{command}</code>\n"
result_text += f"<b>Exit Code:</b> <code>{process.returncode}</code>\n"
result_text += f"<b>Execution Time:</b> <code>{execution_time:.2f}s</code>\n"
if "-t" in message.flags:
result_text += f"<b>Started:</b> <code>{start_time.strftime('%H:%M:%S')}</code>\n"
result_text += f"<b>Finished:</b> <code>{end_time.strftime('%H:%M:%S')}</code>\n"
result_text += "\n"
# Add stdout if present
if stdout_text:
if len(stdout_text) > 3000:
stdout_text = stdout_text[:3000] + "\n... (output truncated)"
result_text += f"<b>📤 Output:</b>\n<pre>{stdout_text}</pre>\n"
# Add stderr if present
if stderr_text:
if len(stderr_text) > 1000:
stderr_text = stderr_text[:1000] + "\n... (error truncated)"
result_text += f"<b>❌ Error:</b>\n<pre>{stderr_text}</pre>\n"
# If no output
if not stdout_text and not stderr_text:
result_text += "<i>No output produced.</i>\n"
# Success indicator
if process.returncode == 0:
result_text += "\n✅ <b>Command executed successfully!</b>"
else:
result_text += f"\n❌ <b>Command failed with exit code {process.returncode}</b>"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Error executing command:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="exec", allow_sudo=False)
async def python_exec(bot: BOT, message: Message):
"""
CMD: EXEC
INFO: Execute Python code safely (Owner only).
USAGE: .exec print("Hello World")
"""
# Only allow owner
if message.from_user.id != Config.OWNER_ID:
await message.reply("❌ <b>Access Denied:</b> Only the bot owner can execute Python code.")
return
code = message.filtered_input
if not code:
await message.reply("❌ <b>No code provided.</b>\nUsage: <code>.exec [python_code]</code>")
return
response = await message.reply(f"🐍 <b>Executing Python code...</b>\n<pre>{code}</pre>")
try:
# Capture output
import io
import sys
from contextlib import redirect_stdout, redirect_stderr
stdout_capture = io.StringIO()
stderr_capture = io.StringIO()
start_time = datetime.now()
# Execute the code
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
exec(code)
end_time = datetime.now()
execution_time = (end_time - start_time).total_seconds()
# Get captured output
stdout_output = stdout_capture.getvalue()
stderr_output = stderr_capture.getvalue()
result_text = f"🐍 <b>Python Execution Result</b>\n\n"
result_text += f"<b>Execution Time:</b> <code>{execution_time:.3f}s</code>\n\n"
if stdout_output:
if len(stdout_output) > 3500:
stdout_output = stdout_output[:3500] + "\n... (output truncated)"
result_text += f"<b>📤 Output:</b>\n<pre>{stdout_output}</pre>\n"
if stderr_output:
if len(stderr_output) > 1000:
stderr_output = stderr_output[:1000] + "\n... (error truncated)"
result_text += f"<b>❌ Error:</b>\n<pre>{stderr_output}</pre>\n"
if not stdout_output and not stderr_output:
result_text += "<i>Code executed without output.</i>\n"
result_text += "\n✅ <b>Python code executed successfully!</b>"
await response.edit(result_text)
except Exception as e:
error_text = f"🐍 <b>Python Execution Error</b>\n\n"
error_text += f"<b>Error:</b> <code>{type(e).__name__}: {str(e)}</code>\n"
error_text += f"<b>Code:</b>\n<pre>{code}</pre>"
await response.edit(error_text)
@BOT.add_cmd(cmd="eval", allow_sudo=False)
async def python_eval(bot: BOT, message: Message):
"""
CMD: EVAL
INFO: Evaluate Python expressions safely (Owner only).
USAGE: .eval 2 + 2
"""
# Only allow owner
if message.from_user.id != Config.OWNER_ID:
await message.reply("❌ <b>Access Denied:</b> Only the bot owner can evaluate Python expressions.")
return
expression = message.filtered_input
if not expression:
await message.reply("❌ <b>No expression provided.</b>\nUsage: <code>.eval [expression]</code>")
return
response = await message.reply(f"🧮 <b>Evaluating:</b> <code>{expression}</code>")
try:
start_time = datetime.now()
# Evaluate the expression
result = eval(expression)
end_time = datetime.now()
execution_time = (end_time - start_time).total_seconds()
result_text = f"🧮 <b>Python Evaluation Result</b>\n\n"
result_text += f"<b>Expression:</b> <code>{expression}</code>\n"
result_text += f"<b>Result:</b> <code>{repr(result)}</code>\n"
result_text += f"<b>Type:</b> <code>{type(result).__name__}</code>\n"
result_text += f"<b>Execution Time:</b> <code>{execution_time:.3f}s</code>\n"
result_text += "\n✅ <b>Expression evaluated successfully!</b>"
await response.edit(result_text)
except Exception as e:
error_text = f"🧮 <b>Python Evaluation Error</b>\n\n"
error_text += f"<b>Expression:</b> <code>{expression}</code>\n"
error_text += f"<b>Error:</b> <code>{type(e).__name__}: {str(e)}</code>"
await response.edit(error_text)

View File

@@ -0,0 +1,180 @@
import asyncio
import subprocess
from datetime import datetime
from app import BOT, Message
@BOT.add_cmd(cmd="speedtest")
async def internet_speedtest(bot: BOT, message: Message):
"""
CMD: SPEEDTEST
INFO: Test internet connection speed using speedtest-cli.
USAGE: .speedtest
"""
response = await message.reply("🔄 <b>Starting internet speed test...</b>\n<i>This may take a few moments...</i>")
try:
# Update status
await response.edit("🔍 <b>Finding best server...</b>")
# Run speedtest command
process = await asyncio.create_subprocess_exec(
'speedtest-cli', '--simple', '--secure',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode().strip()
await response.edit(f"❌ <b>Speedtest failed:</b>\n<code>{error_msg}</code>")
return
# Parse results
output = stdout.decode().strip()
lines = output.split('\n')
# Extract values
ping_line = next((line for line in lines if 'Ping:' in line), '')
download_line = next((line for line in lines if 'Download:' in line), '')
upload_line = next((line for line in lines if 'Upload:' in line), '')
# Parse values
ping = ping_line.split(':')[1].strip() if ping_line else 'N/A'
download = download_line.split(':')[1].strip() if download_line else 'N/A'
upload = upload_line.split(':')[1].strip() if upload_line else 'N/A'
# Get current time
test_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
result_text = f"""🚀 <b>Internet Speed Test Results</b>
📊 <b>Test Results:</b>
<b>📡 Ping:</b> {ping}
<b>⬇️ Download:</b> {download}
<b>⬆️ Upload:</b> {upload}
🕐 <b>Test Time:</b> {test_time}
🔧 <b>Powered by:</b> speedtest.net
<i>Test completed successfully! ✅</i>"""
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Error running speedtest:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="speedtest-server")
async def speedtest_with_server(bot: BOT, message: Message):
"""
CMD: SPEEDTEST-SERVER
INFO: Test internet speed with detailed server information.
USAGE: .speedtest-server
"""
response = await message.reply("🔄 <b>Starting detailed speed test...</b>")
try:
# Run detailed speedtest
process = await asyncio.create_subprocess_exec(
'speedtest-cli', '--secure',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode().strip()
await response.edit(f"❌ <b>Speedtest failed:</b>\n<code>{error_msg}</code>")
return
# Parse detailed output
output = stdout.decode().strip()
# Extract server info, speeds, etc.
lines = output.split('\n')
server_info = ""
results_info = ""
for line in lines:
if 'Testing from' in line:
server_info += f"<b>ISP:</b> {line.split('Testing from')[1].strip()}\n"
elif 'Hosted by' in line:
server_info += f"<b>Server:</b> {line.split('Hosted by')[1].strip()}\n"
elif 'Download:' in line:
results_info += f"<b>⬇️ Download:</b> {line.split('Download:')[1].strip()}\n"
elif 'Upload:' in line:
results_info += f"<b>⬆️ Upload:</b> {line.split('Upload:')[1].strip()}\n"
# Get share URL if available
share_url = ""
for line in lines:
if 'Share results:' in line:
share_url = f"\n<b>🔗 Share URL:</b> {line.split('Share results:')[1].strip()}"
break
test_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
detailed_result = f"""🚀 <b>Detailed Internet Speed Test</b>
🌐 <b>Connection Info:</b>
{server_info}
📊 <b>Speed Results:</b>
{results_info}
🕐 <b>Test Time:</b> {test_time}{share_url}
<i>Detailed test completed! ✅</i>"""
await response.edit(detailed_result)
except Exception as e:
await response.edit(f"❌ <b>Error running detailed speedtest:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="speedtest-list")
async def speedtest_servers_list(bot: BOT, message: Message):
"""
CMD: SPEEDTEST-LIST
INFO: List available speedtest servers.
USAGE: .speedtest-list
"""
response = await message.reply("🔄 <b>Getting available servers...</b>")
try:
# Get server list
process = await asyncio.create_subprocess_exec(
'speedtest-cli', '--list',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode != 0:
error_msg = stderr.decode().strip()
await response.edit(f"❌ <b>Failed to get server list:</b>\n<code>{error_msg}</code>")
return
output = stdout.decode().strip()
lines = output.split('\n')
# Take only first 15 servers to avoid message being too long
server_lines = [line for line in lines if line.strip() and ')' in line][:15]
servers_text = "🌐 <b>Available Speedtest Servers</b>\n\n"
for line in server_lines:
servers_text += f"<code>{line.strip()}</code>\n"
servers_text += f"\n<i>Showing first 15 servers. Use speedtest-cli --server [ID] for specific server.</i>"
await response.edit(servers_text)
except Exception as e:
await response.edit(f"❌ <b>Error getting server list:</b>\n<code>{str(e)}</code>")

View File

@@ -0,0 +1,433 @@
import platform
import psutil
import os
import subprocess
from datetime import datetime, timedelta
from app import BOT, Message
def is_termux():
"""Check if running on Termux (Android)"""
return os.path.exists("/data/data/com.termux") or "TERMUX_VERSION" in os.environ
def get_android_info():
"""Get Android-specific information"""
android_info = {}
try:
# Try to get Android version
if os.path.exists("/system/build.prop"):
with open("/system/build.prop", "r") as f:
for line in f:
if "ro.build.version.release" in line:
android_info["version"] = line.split("=")[1].strip()
elif "ro.product.model" in line:
android_info["model"] = line.split("=")[1].strip()
elif "ro.product.brand" in line:
android_info["brand"] = line.split("=")[1].strip()
except:
pass
# Try using getprop command
try:
result = subprocess.run(["getprop", "ro.build.version.release"],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
android_info["version"] = result.stdout.strip()
except:
pass
try:
result = subprocess.run(["getprop", "ro.product.model"],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
android_info["model"] = result.stdout.strip()
except:
pass
return android_info
@BOT.add_cmd(cmd="sysinfo")
async def system_info(bot: BOT, message: Message):
"""
CMD: SYSINFO
INFO: Get detailed system information including CPU, RAM, disk usage, and uptime.
USAGE: .sysinfo
"""
response = await message.reply("🔄 <b>Gathering system information...</b>")
try:
# Detect if running on Termux/Android
is_android = is_termux()
# System Info
system = platform.system()
node = platform.node()
release = platform.release()
version = platform.version()
machine = platform.machine()
processor = platform.processor() or "Unknown"
# Get Android-specific info if applicable
android_info = {}
if is_android:
android_info = get_android_info()
# CPU Info with fallbacks
try:
cpu_count = psutil.cpu_count(logical=False) or psutil.cpu_count(logical=True)
cpu_count_logical = psutil.cpu_count(logical=True)
except:
cpu_count = "Unknown"
cpu_count_logical = "Unknown"
try:
cpu_freq = psutil.cpu_freq()
if cpu_freq:
freq_current = cpu_freq.current
freq_max = cpu_freq.max
else:
freq_current = freq_max = None
except:
freq_current = freq_max = None
try:
cpu_percent = psutil.cpu_percent(interval=1)
except:
cpu_percent = "Unknown"
# Memory Info with fallbacks
try:
memory = psutil.virtual_memory()
except:
memory = None
try:
swap = psutil.swap_memory()
except:
swap = None
# Disk Info with fallbacks
try:
# Try different paths for Termux
if is_android:
# Termux typically uses /data/data/com.termux
disk_paths = ["/data/data/com.termux", "/", "/sdcard"]
disk = None
for path in disk_paths:
try:
if os.path.exists(path):
disk = psutil.disk_usage(path)
break
except:
continue
else:
disk = psutil.disk_usage('/')
except:
disk = None
# Boot time and uptime with fallbacks
try:
boot_time = psutil.boot_time()
boot_time_str = datetime.fromtimestamp(boot_time).strftime("%Y-%m-%d %H:%M:%S")
uptime = datetime.now() - datetime.fromtimestamp(boot_time)
uptime_str = str(uptime).split('.')[0] # Remove microseconds
except:
boot_time_str = "Unknown"
uptime_str = "Unknown"
# Load average (Unix systems) with fallbacks
try:
load_avg = psutil.getloadavg()
load_str = f"<b>Load Average:</b> {load_avg[0]:.2f}, {load_avg[1]:.2f}, {load_avg[2]:.2f}"
except (AttributeError, OSError):
load_str = "<b>Load Average:</b> Not available on this system"
# Format sizes
def bytes_to_gb(bytes_val):
return bytes_val / (1024**3)
# Build system info with Android detection
if is_android:
system_emoji = "📱"
system_title = "Android System Information (Termux)"
else:
system_emoji = "🖥️"
system_title = "System Information"
info_text = f"""{system_emoji} <b>{system_title}</b>
<b>🔧 System Details:</b>
<b>OS:</b> {system} {release}"""
# Add Android-specific info if available
if is_android and android_info:
if "version" in android_info:
info_text += f"\n<b>Android:</b> {android_info['version']}"
if "brand" in android_info and "model" in android_info:
info_text += f"\n<b>Device:</b> {android_info['brand']} {android_info['model']}"
elif "model" in android_info:
info_text += f"\n<b>Device:</b> {android_info['model']}"
info_text += f"""
<b>Hostname:</b> {node}
<b>Architecture:</b> {machine}
<b>Processor:</b> {processor}
<b>⚡ CPU Information:</b>"""
if cpu_count != "Unknown":
info_text += f"\n<b>Cores:</b> {cpu_count} physical, {cpu_count_logical} logical"
else:
info_text += f"\n<b>Cores:</b> {cpu_count_logical} logical"
if freq_current and freq_max:
info_text += f"\n<b>Frequency:</b> {freq_current:.0f} MHz (Max: {freq_max:.0f} MHz)"
elif freq_current:
info_text += f"\n<b>Frequency:</b> {freq_current:.0f} MHz"
else:
info_text += f"\n<b>Frequency:</b> Not available"
info_text += f"\n<b>Usage:</b> {cpu_percent}%"
info_text += f"\n\n<b>🧠 Memory Information:</b>"
if memory:
info_text += f"""
<b>Total RAM:</b> {bytes_to_gb(memory.total):.2f} GB
<b>Available:</b> {bytes_to_gb(memory.available):.2f} GB ({memory.percent}% used)"""
else:
info_text += f"\n<b>Memory:</b> Information not accessible"
if swap and swap.total > 0:
info_text += f"""
<b>Swap Total:</b> {bytes_to_gb(swap.total):.2f} GB
<b>Swap Used:</b> {bytes_to_gb(swap.used):.2f} GB ({swap.percent}% used)"""
elif is_android:
info_text += f"\n<b>Swap:</b> Not typically used on Android"
else:
info_text += f"\n<b>Swap:</b> Not available"
info_text += f"\n\n<b>💾 Disk Information:</b>"
if disk:
storage_path = "Termux Storage" if is_android else "Root"
info_text += f"""
<b>Path:</b> {storage_path}
<b>Total:</b> {bytes_to_gb(disk.total):.2f} GB
<b>Used:</b> {bytes_to_gb(disk.used):.2f} GB ({disk.percent}% used)
<b>Free:</b> {bytes_to_gb(disk.free):.2f} GB"""
else:
info_text += f"\n<b>Storage:</b> Information not accessible"
info_text += f"""
<b>⏱️ System Uptime:</b>
<b>Boot Time:</b> {boot_time_str}
<b>Uptime:</b> {uptime_str}
{load_str}"""
# Add Termux-specific note
if is_android:
info_text += f"\n\n<i>📱 Running on Android via Termux</i>"
await response.edit(info_text)
except Exception as e:
await response.edit(f"❌ <b>Error getting system info:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="disk")
async def disk_usage(bot: BOT, message: Message):
"""
CMD: DISK
INFO: Show disk usage for all mounted drives.
USAGE: .disk
"""
response = await message.reply("🔄 <b>Getting disk usage...</b>")
try:
is_android = is_termux()
if is_android:
# Special handling for Termux/Android
disk_info = "💾 <b>Storage Usage Information (Termux)</b>\n\n"
# Check common Termux paths
termux_paths = [
("/data/data/com.termux", "Termux App Storage"),
("/sdcard", "Internal Storage"),
("/storage/emulated/0", "Emulated Storage"),
("/", "Root")
]
for path, description in termux_paths:
try:
if os.path.exists(path):
usage = psutil.disk_usage(path)
total_gb = usage.total / (1024**3)
used_gb = usage.used / (1024**3)
free_gb = usage.free / (1024**3)
percent = (usage.used / usage.total) * 100 if usage.total > 0 else 0
# Create progress bar
bar_length = 15
filled_length = int(bar_length * percent / 100)
bar = "" * filled_length + "" * (bar_length - filled_length)
disk_info += f"""<b>📱 {description}</b>
<b>Path:</b> {path}
<b>Total:</b> {total_gb:.2f} GB
<b>Used:</b> {used_gb:.2f} GB ({percent:.1f}%)
<b>Free:</b> {free_gb:.2f} GB
{bar} {percent:.1f}%
"""
except (PermissionError, OSError):
disk_info += f"<b>📱 {description}</b>\n❌ Access denied or not mounted\n\n"
else:
# Regular disk usage for non-Android systems
try:
partitions = psutil.disk_partitions()
except:
partitions = []
disk_info = "💾 <b>Disk Usage Information</b>\n\n"
for partition in partitions:
try:
usage = psutil.disk_usage(partition.mountpoint)
total_gb = usage.total / (1024**3)
used_gb = usage.used / (1024**3)
free_gb = usage.free / (1024**3)
percent = (usage.used / usage.total) * 100
# Create a simple progress bar
bar_length = 15
filled_length = int(bar_length * percent / 100)
bar = "" * filled_length + "" * (bar_length - filled_length)
disk_info += f"""<b>📁 {partition.mountpoint}</b>
<b>Device:</b> {partition.device}
<b>Filesystem:</b> {partition.fstype}
<b>Total:</b> {total_gb:.2f} GB
<b>Used:</b> {used_gb:.2f} GB ({percent:.1f}%)
<b>Free:</b> {free_gb:.2f} GB
{bar} {percent:.1f}%
"""
except PermissionError:
disk_info += f"<b>📁 {partition.mountpoint}</b>\n❌ Permission denied\n\n"
await response.edit(disk_info)
except Exception as e:
await response.edit(f"❌ <b>Error getting disk usage:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="mem")
async def memory_info(bot: BOT, message: Message):
"""
CMD: MEM
INFO: Show detailed memory usage information.
USAGE: .mem
"""
response = await message.reply("🔄 <b>Getting memory information...</b>")
try:
memory = psutil.virtual_memory()
swap = psutil.swap_memory()
def bytes_to_gb(bytes_val):
return bytes_val / (1024**3)
def bytes_to_mb(bytes_val):
return bytes_val / (1024**2)
# Memory progress bar
mem_percent = memory.percent
bar_length = 20
filled_length = int(bar_length * mem_percent / 100)
mem_bar = "" * filled_length + "" * (bar_length - filled_length)
# Swap progress bar
swap_percent = swap.percent
swap_filled_length = int(bar_length * swap_percent / 100)
swap_bar = "" * swap_filled_length + "" * (bar_length - swap_filled_length)
mem_text = f"""🧠 <b>Memory Usage Information</b>
<b>📊 Virtual Memory:</b>
<b>Total:</b> {bytes_to_gb(memory.total):.2f} GB
<b>Available:</b> {bytes_to_gb(memory.available):.2f} GB
<b>Used:</b> {bytes_to_gb(memory.used):.2f} GB
<b>Cached:</b> {bytes_to_mb(memory.cached):.0f} MB
<b>Buffers:</b> {bytes_to_mb(memory.buffers):.0f} MB
<b>Usage:</b> {mem_percent}%
{mem_bar} {mem_percent}%
<b>💿 Swap Memory:</b>
<b>Total:</b> {bytes_to_gb(swap.total):.2f} GB
<b>Used:</b> {bytes_to_gb(swap.used):.2f} GB
<b>Free:</b> {bytes_to_gb(swap.free):.2f} GB
<b>Usage:</b> {swap_percent}%
{swap_bar} {swap_percent}%"""
await response.edit(mem_text)
except Exception as e:
await response.edit(f"❌ <b>Error getting memory info:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="cpu")
async def cpu_info(bot: BOT, message: Message):
"""
CMD: CPU
INFO: Show detailed CPU usage and information.
USAGE: .cpu
"""
response = await message.reply("🔄 <b>Getting CPU information...</b>")
try:
# Get CPU info
cpu_percent = psutil.cpu_percent(interval=1, percpu=True)
cpu_count_physical = psutil.cpu_count(logical=False)
cpu_count_logical = psutil.cpu_count(logical=True)
cpu_freq = psutil.cpu_freq()
# Overall CPU usage
overall_cpu = psutil.cpu_percent(interval=1)
# CPU progress bar
bar_length = 20
filled_length = int(bar_length * overall_cpu / 100)
cpu_bar = "" * filled_length + "" * (bar_length - filled_length)
cpu_text = f"""⚡ <b>CPU Usage Information</b>
<b>🖥️ General Info:</b>
<b>Physical Cores:</b> {cpu_count_physical}
<b>Logical Cores:</b> {cpu_count_logical}
<b>Current Frequency:</b> {cpu_freq.current:.0f} MHz
<b>Max Frequency:</b> {cpu_freq.max:.0f} MHz
<b>Min Frequency:</b> {cpu_freq.min:.0f} MHz
<b>📊 Overall Usage:</b> {overall_cpu}%
{cpu_bar} {overall_cpu}%
<b>🔄 Per-Core Usage:</b>"""
# Add per-core usage
for i, percent in enumerate(cpu_percent):
core_filled = int(10 * percent / 100) # Smaller bars for cores
core_bar = "" * core_filled + "" * (10 - core_filled)
cpu_text += f"\n<b>Core {i+1}:</b> {core_bar} {percent}%"
await response.edit(cpu_text)
except Exception as e:
await response.edit(f"❌ <b>Error getting CPU info:</b>\n<code>{str(e)}</code>")

View File

@@ -0,0 +1,295 @@
import os
import subprocess
import json
from pathlib import Path
from app import BOT, Message
def is_termux():
"""Check if running on Termux (Android)"""
return os.path.exists("/data/data/com.termux") or "TERMUX_VERSION" in os.environ
@BOT.add_cmd(cmd="termux")
async def termux_info(bot: BOT, message: Message):
"""
CMD: TERMUX
INFO: Show Termux and Android-specific information.
USAGE: .termux
"""
if not is_termux():
await message.reply("❌ <b>This command only works on Termux (Android)!</b>")
return
response = await message.reply("📱 <b>Gathering Termux information...</b>")
try:
termux_info = "📱 <b>Termux & Android Information</b>\n\n"
# Termux version
try:
termux_version = os.environ.get("TERMUX_VERSION", "Unknown")
termux_info += f"<b>🤖 Termux Version:</b> {termux_version}\n"
except:
pass
# Android version and device info
android_props = {}
prop_commands = {
"Android Version": "ro.build.version.release",
"Android SDK": "ro.build.version.sdk",
"Device Model": "ro.product.model",
"Device Brand": "ro.product.brand",
"Device Name": "ro.product.name",
"CPU ABI": "ro.product.cpu.abi",
"Build ID": "ro.build.id",
"Build Date": "ro.build.date"
}
for display_name, prop in prop_commands.items():
try:
result = subprocess.run(["getprop", prop],
capture_output=True, text=True, timeout=3)
if result.returncode == 0 and result.stdout.strip():
android_props[display_name] = result.stdout.strip()
except:
pass
if android_props:
termux_info += f"\n<b>📋 Android Properties:</b>\n"
for name, value in android_props.items():
termux_info += f"<b>{name}:</b> <code>{value}</code>\n"
# Termux environment variables
termux_vars = {}
important_vars = [
"TERMUX_VERSION", "PREFIX", "HOME", "TMPDIR",
"LD_LIBRARY_PATH", "PATH", "ANDROID_DATA", "ANDROID_ROOT"
]
for var in important_vars:
value = os.environ.get(var)
if value:
termux_vars[var] = value
if termux_vars:
termux_info += f"\n<b>🔧 Termux Environment:</b>\n"
for var, value in termux_vars.items():
# Truncate long paths
if len(value) > 50:
value = value[:47] + "..."
termux_info += f"<b>{var}:</b> <code>{value}</code>\n"
# Package information
try:
pkg_result = subprocess.run(["pkg", "list-installed"],
capture_output=True, text=True, timeout=5)
if pkg_result.returncode == 0:
packages = pkg_result.stdout.strip().split('\n')
pkg_count = len([p for p in packages if p.strip()])
termux_info += f"\n<b>📦 Installed Packages:</b> {pkg_count}\n"
except:
pass
# Storage information
termux_info += f"\n<b>💾 Storage Paths:</b>\n"
storage_paths = [
("$HOME", os.path.expanduser("~"), "Termux Home"),
("$PREFIX", os.environ.get("PREFIX", "/data/data/com.termux/files/usr"), "Termux Prefix"),
("/sdcard", "/sdcard", "Internal Storage"),
("/storage/emulated/0", "/storage/emulated/0", "Emulated Storage")
]
for var_name, path, description in storage_paths:
if os.path.exists(path):
try:
import psutil
usage = psutil.disk_usage(path)
free_gb = usage.free / (1024**3)
total_gb = usage.total / (1024**3)
termux_info += f"<b>{description}:</b> {free_gb:.1f}GB free / {total_gb:.1f}GB total\n"
except:
termux_info += f"<b>{description}:</b> {path}\n"
else:
termux_info += f"<b>{description}:</b> {path}\n"
# Permissions and capabilities
termux_info += f"\n<b>🔐 Capabilities:</b>\n"
capabilities = [
("Storage Access", "/sdcard", "Can access internal storage"),
("Network Access", None, "Internet connectivity"),
("Root Access", "/system", "System directory access"),
("Termux-API", None, "Android API access")
]
for cap_name, test_path, description in capabilities:
if cap_name == "Network Access":
# Test basic network
try:
import socket
socket.create_connection(("8.8.8.8", 53), timeout=3)
status = ""
except:
status = ""
elif cap_name == "Termux-API":
# Check if termux-api is available
try:
result = subprocess.run(["termux-telephony-deviceinfo"],
capture_output=True, timeout=3)
status = "" if result.returncode == 0 else ""
except:
status = ""
elif test_path:
status = "" if os.path.exists(test_path) else ""
else:
status = ""
termux_info += f"<b>{cap_name}:</b> {status} <i>{description}</i>\n"
# Python environment
termux_info += f"\n<b>🐍 Python Environment:</b>\n"
try:
import sys
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
termux_info += f"<b>Python Version:</b> {python_version}\n"
termux_info += f"<b>Python Path:</b> <code>{sys.executable}</code>\n"
# Check important modules
modules_to_check = ["psutil", "requests", "pyrogram", "qrcode"]
available_modules = []
for module in modules_to_check:
try:
__import__(module)
available_modules.append(f"{module}")
except ImportError:
available_modules.append(f"{module}")
termux_info += f"<b>Key Modules:</b> {', '.join(available_modules)}\n"
except Exception as e:
termux_info += f"<b>Python:</b> Error getting info\n"
termux_info += f"\n<i>📱 Termux information collected successfully!</i>"
await response.edit(termux_info)
except Exception as e:
await response.edit(f"❌ <b>Error getting Termux info:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="androidinfo")
async def android_device_info(bot: BOT, message: Message):
"""
CMD: ANDROIDINFO
INFO: Get detailed Android device information.
USAGE: .androidinfo
"""
if not is_termux():
await message.reply("❌ <b>This command only works on Termux (Android)!</b>")
return
response = await message.reply("📱 <b>Getting Android device info...</b>")
try:
device_info = "📱 <b>Android Device Information</b>\n\n"
# Comprehensive device properties
device_props = {
"Device Information": [
("ro.product.brand", "Brand"),
("ro.product.manufacturer", "Manufacturer"),
("ro.product.model", "Model"),
("ro.product.name", "Product Name"),
("ro.product.device", "Device Codename")
],
"Android System": [
("ro.build.version.release", "Android Version"),
("ro.build.version.sdk", "SDK Level"),
("ro.build.version.security_patch", "Security Patch"),
("ro.build.id", "Build ID"),
("ro.build.type", "Build Type"),
("ro.build.tags", "Build Tags")
],
"Hardware": [
("ro.product.cpu.abi", "CPU Architecture"),
("ro.product.cpu.abilist", "Supported ABIs"),
("ro.board.platform", "Platform"),
("ro.hardware", "Hardware"),
("ro.revision", "Hardware Revision")
],
"Display": [
("ro.sf.lcd_density", "LCD Density"),
("ro.config.small_battery", "Small Battery"),
("ro.config.low_ram", "Low RAM Device")
]
}
for category, props in device_props.items():
device_info += f"<b>📋 {category}:</b>\n"
for prop, display_name in props:
try:
result = subprocess.run(["getprop", prop],
capture_output=True, text=True, timeout=3)
if result.returncode == 0 and result.stdout.strip():
value = result.stdout.strip()
# Truncate very long values
if len(value) > 40:
value = value[:37] + "..."
device_info += f" <b>{display_name}:</b> <code>{value}</code>\n"
except:
pass
device_info += "\n"
# Memory information (from /proc/meminfo if accessible)
try:
if os.path.exists("/proc/meminfo"):
with open("/proc/meminfo", "r") as f:
meminfo = f.read()
for line in meminfo.split('\n')[:3]: # First 3 lines usually contain key info
if line.strip():
device_info += f"<b>💾 {line.strip()}</b>\n"
device_info += "\n"
except:
pass
# Battery information (if termux-api is available)
try:
result = subprocess.run(["termux-battery-status"],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
battery_data = json.loads(result.stdout)
device_info += f"<b>🔋 Battery Information:</b>\n"
device_info += f" <b>Level:</b> {battery_data.get('percentage', 'Unknown')}%\n"
device_info += f" <b>Status:</b> {battery_data.get('status', 'Unknown')}\n"
device_info += f" <b>Health:</b> {battery_data.get('health', 'Unknown')}\n"
device_info += f" <b>Temperature:</b> {battery_data.get('temperature', 'Unknown')}°C\n\n"
except:
pass
# Network information
try:
result = subprocess.run(["termux-wifi-connectioninfo"],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
wifi_data = json.loads(result.stdout)
device_info += f"<b>📶 WiFi Information:</b>\n"
device_info += f" <b>SSID:</b> {wifi_data.get('ssid', 'Unknown')}\n"
device_info += f" <b>IP Address:</b> {wifi_data.get('ip', 'Unknown')}\n"
device_info += f" <b>Signal:</b> {wifi_data.get('rssi', 'Unknown')} dBm\n\n"
except:
pass
device_info += f"<i>📱 Device information collected via Termux API and getprop</i>"
await response.edit(device_info)
except Exception as e:
await response.edit(f"❌ <b>Error getting Android info:</b>\n<code>{str(e)}</code>")

View File

@@ -0,0 +1,61 @@
import asyncio
import os
from pyrogram.errors import BadRequest
from ub_core.utils import get_name
from app import BOT, Message
@BOT.add_cmd(cmd="ids")
async def get_ids(bot: BOT, message: Message) -> None:
reply: Message = message.replied
if reply:
resp_str: str = ""
reply_user, reply_forward = reply.forward_from_chat, reply.from_user
resp_str += f"<b>{get_name(reply.chat)}</b>: <code>{reply.chat.id}</code>\n"
if reply_forward:
resp_str += f"<b>{get_name(reply_forward)}</b>: <code>{reply_forward.id}</code>\n"
if reply_user:
resp_str += f"<b>{get_name(reply_user)}</b>: <code>{reply_user.id}</code>"
elif message.input:
resp_str: int = (await bot.get_chat(message.input[1:])).id
else:
resp_str: str = f"<b>{get_name(message.chat)}</b>: <code>{message.chat.id}</code>"
await message.reply(resp_str)
@BOT.add_cmd(cmd="join")
async def join_chat(bot: BOT, message: Message) -> None:
chat: str = message.input
try:
await bot.join_chat(chat)
except (KeyError, BadRequest):
try:
await bot.join_chat(os.path.basename(chat).strip())
except Exception as e:
await message.reply(str(e))
return
await message.reply("Joined")
@BOT.add_cmd(cmd="leave")
async def leave_chat(bot: BOT, message: Message) -> None:
if message.input:
chat = message.input
else:
chat = message.chat.id
await message.reply(
text=f"Leaving current chat in 5\nReply with `{message.trigger}c` to cancel",
del_in=5,
block=True,
)
await asyncio.sleep(5)
try:
await bot.leave_chat(chat)
except Exception as e:
await message.reply(str(e))

View File

@@ -0,0 +1,14 @@
from app import BOT, Message
@BOT.add_cmd(cmd="click")
async def click(bot: BOT, message: Message):
if not message.input or not message.replied:
await message.reply("reply to a message containing a button and give a button to click")
return
try:
button_name = message.input.strip()
button = int(button_name) if button_name.isdigit() else button_name
await message.replied.click(button)
except Exception as e:
await message.reply(str(e), del_in=5)

View File

@@ -0,0 +1,77 @@
import asyncio
from pyrogram.enums import ChatType
from ub_core.utils.helpers import create_chunks
from app import BOT, Message
from app.plugins.tg_tools.get_message import parse_link
@BOT.add_cmd(cmd="del")
async def delete_message(bot: BOT, message: Message) -> None:
"""
CMD: DEL
INFO: Delete the replied message.
FLAGS: -r to remotely delete a text using its link.
USAGE:
.del | .del -r t.me/......
"""
if "-r" in message.flags:
chat_id, _, message_id = parse_link(message.filtered_input)
await bot.delete_messages(chat_id=chat_id, message_ids=message_id, revoke=True)
return
await message.delete(reply=True)
@BOT.add_cmd(cmd="purge")
async def purge_(bot: BOT, message: Message) -> None:
"""
CMD: PURGE
INFO: DELETE MULTIPLE MESSAGES
USAGE:
.purge [reply to message]
"""
chat_id = message.chat.id
start_message: int = message.reply_id
# Not replied to a message
if not start_message:
await message.reply("Reply to a message.")
return
# Replied was topic creation message
if message.thread_origin_message:
await message.reply("Reply to a message.")
return
# Get Topic messages till replied
if message.is_topic_message:
message_ids = []
async for _message in bot.get_discussion_replies(
chat_id=message.chat.id, message_id=message.message_thread_id, limit=100
):
message_ids.append(_message.id)
if _message.id == message.reply_id or len(message_ids) > 100:
break
else:
# Generate Message Ids
message_ids: list[int] = list(range(start_message, message.id))
# Get messages from server if chat is private or ids are too big.
if message.chat.type in {ChatType.PRIVATE, ChatType.BOT} or len(message_ids) > 100:
messages = await bot.get_messages(chat_id=chat_id, message_ids=message_ids, replies=0)
message_ids = [message.id for message in messages]
# Perform Quick purge of bigger chunks
if len(message_ids) < 100:
chunk_size = 50
sleep_interval = 2
else:
chunk_size = 25
sleep_interval = 5
for chunk in create_chunks(message_ids, chunk_size=chunk_size):
await bot.delete_messages(chat_id=chat_id, message_ids=chunk, revoke=True)
await asyncio.sleep(sleep_interval)

View File

@@ -0,0 +1,50 @@
from urllib.parse import urlparse
from app import BOT, Message
def parse_link(link: str) -> tuple[int | str, int, int]:
parsed_url: str = urlparse(link).path.strip("/")
link_chunks = parsed_url.lstrip("c/").split("/")
thread = "0"
if len(link_chunks) == 3:
chat, thread, message = link_chunks
else:
chat, message = link_chunks
if chat.isdigit():
chat = int(f"-100{chat}")
if thread.isdigit():
thread = int(thread)
return chat, thread, int(message)
@BOT.add_cmd(cmd="gm")
async def get_message(bot: BOT, message: Message):
"""
CMD: Get Message
INFO: Get a Message Json/Attr by providing link.
USAGE:
.gm t.me/.... | .gm t.me/... text [Returns message text]
"""
if not message.input:
await message.reply("Give a Message link.")
return
attr = None
if len(message.text_list) == 3:
link, attr = message.text_list[1:]
else:
link = message.input.strip()
remote_message = Message(await bot.get_messages(link=link))
if not attr:
await message.reply(f"```\n{remote_message}```")
return
await message.reply(f"```\n{getattr(remote_message, attr, None)}```")

View File

@@ -0,0 +1,228 @@
import asyncio
import os
import random
import shutil
import time
from io import BytesIO
from pathlib import Path
from PIL import Image
from pyrogram.enums import MessageMediaType
from pyrogram.errors import StickersetInvalid
from pyrogram.raw import functions
from pyrogram.raw import types as raw_types
from pyrogram.raw.base.messages import StickerSet as BaseStickerSet
from pyrogram.types import User
from pyrogram.utils import FileId
from ub_core import utils as core_utils
from app import BOT, Config, Message, bot, extra_config
EMOJIS = ("", "🤡", "🙂", "🤔", "🔪", "😂", "💀")
async def save_sticker(file: Path | BytesIO) -> str:
client = getattr(bot, "bot", bot)
sent_file = await client.send_document(
chat_id=Config.LOG_CHAT, document=file, message_thread_id=Config.LOG_CHAT_THREAD_ID
)
if isinstance(file, Path) and file.is_file():
shutil.rmtree(file.parent, ignore_errors=True)
return sent_file.document.file_id
def resize_photo(input_file: BytesIO) -> BytesIO:
image = Image.open(input_file)
maxsize = 512
scale = maxsize / max(image.width, image.height)
new_size = (int(image.width * scale), int(image.height * scale))
image = image.resize(new_size, Image.LANCZOS)
resized_photo = BytesIO()
resized_photo.name = "sticker.png"
image.save(resized_photo, format="PNG")
return resized_photo
async def photo_kang(message: Message, **_) -> tuple[str, None]:
file = await message.download(in_memory=True)
file.seek(0)
resized_file = await asyncio.to_thread(resize_photo, file)
return await save_sticker(resized_file), None
async def video_kang(message: Message, ff=False) -> tuple[str, None]:
video = message.video or message.animation or message.document
if video.file_size > 5242880:
raise MemoryError("File Size exceeds 5MB.")
download_path = Path("downloads") / str(time.time())
input_file = download_path / "input.mp4"
output_file = download_path / "sticker.webm"
download_path.mkdir(parents=True, exist_ok=True)
await message.download(str(input_file))
duration = getattr(video, "duration", None)
if not duration:
duration = await core_utils.get_duration(file=str(input_file))
await resize_video(input_file=input_file, output_file=output_file, duration=duration, ff=ff)
return await save_sticker(output_file), None
async def resize_video(
input_file: Path | str, output_file: Path | str, duration: int, ff: bool = False
):
cmd = f"ffmpeg -hide_banner -loglevel error -i '{input_file}' -vf "
if ff:
cmd += '"scale=w=512:h=512:force_original_aspect_ratio=decrease,setpts=0.3*PTS" '
cmd += "-ss 0 -t 3 -r 30 -loop 0 -an -c:v libvpx-vp9 -b:v 256k -fs 256k "
elif duration < 3:
cmd += '"scale=w=512:h=512:force_original_aspect_ratio=decrease" '
cmd += "-ss 0 -r 30 -an -c:v libvpx-vp9 -b:v 256k -fs 256k "
else:
cmd += '"scale=w=512:h=512:force_original_aspect_ratio=decrease" '
cmd += "-ss 0 -t 3 -r 30 -an -c:v libvpx-vp9 -b:v 256k -fs 256k "
await core_utils.run_shell_cmd(cmd=f"{cmd}'{output_file}'")
async def document_kang(message: Message, ff: bool = False) -> tuple[str, None]:
name, ext = os.path.splitext(core_utils.get_tg_media_details(message).file_name)
if ext.lower() in core_utils.MediaExts.PHOTO:
return await photo_kang(message)
elif ext.lower() in {*core_utils.MediaExts.VIDEO, *core_utils.MediaExts.GIF}:
return await video_kang(message=message, ff=ff)
async def sticker_kang(message: Message, **_) -> tuple[str, str]:
sticker = message.sticker
if sticker.is_animated:
raise TypeError("Animated Stickers Not Supported.")
return sticker.file_id, sticker.emoji
MEDIA_TYPE_MAP = {
MessageMediaType.PHOTO: photo_kang,
MessageMediaType.VIDEO: video_kang,
MessageMediaType.ANIMATION: video_kang,
MessageMediaType.DOCUMENT: document_kang,
MessageMediaType.STICKER: sticker_kang,
}
async def get_sticker_set(
client: BOT, user: User
) -> tuple[str, str, bool, raw_types.StickerSet | None]:
count = 0
create_new = False
suffix = f"_by_{client.me.username}" if client.is_bot else ""
while True:
shortname = f"P_UB_{user.id}_mixpack_{count}{suffix}"
try:
sticker_set: BaseStickerSet = await client.invoke(
functions.messages.GetStickerSet(
stickerset=raw_types.InputStickerSetShortName(short_name=shortname),
hash=0,
)
)
sticker_set = sticker_set.set
if sticker_set.count < 120:
break
count += 1
except StickersetInvalid:
create_new = True
sticker_set: BaseStickerSet | None = None
break
if extra_config.CUSTOM_PACK_NAME:
pack_title = extra_config.CUSTOM_PACK_NAME
else:
pack_title = f"{user.username or core_utils.get_name(user)}'s kang pack vol {count}"
return shortname, pack_title, create_new, sticker_set
async def kang_sticker(
client: BOT, media_file_id: str, emoji: str = None, user: User = None
) -> BaseStickerSet:
shortname, pack_title, create_new, sticker_set = await get_sticker_set(client, user)
file_id = FileId.decode(media_file_id)
document = raw_types.InputDocument(
access_hash=file_id.access_hash,
id=file_id.media_id,
file_reference=file_id.file_reference,
)
set_item = raw_types.InputStickerSetItem(
document=document, emoji=emoji or random.choice(EMOJIS)
)
if create_new:
query = functions.stickers.CreateStickerSet(
user_id=await bot.resolve_peer(peer_id=user.id),
short_name=shortname,
title=pack_title,
stickers=[set_item],
)
else:
query = functions.stickers.AddStickerToSet(
stickerset=raw_types.InputStickerSetID(
id=sticker_set.id, access_hash=sticker_set.access_hash
),
sticker=set_item,
)
return await client.invoke(query)
async def kang(bot: BOT, message: Message):
"""
CMD: KANG
INFO: Save a sticker/image/gif/video to your sticker pack.
FLAGS: -f to fastforward video tp fit 3 sec duration.
USAGE: .kang | .kang -f
Diffrences to legacy version:
• Is almost instantaneous because uses built-in methods.
• Sudo users get their own packs.
• If in dual mode pack ownership is given to respective Sudo users.
• Kangs both photo and video stickers into a single pack.
• As a result video stickers are not limited to the limit of 50.
Note: if you still would like to use old style set USE_LEGACY_KANG=1
"""
replied = message.replied
media_func = MEDIA_TYPE_MAP.get(replied.media)
if not media_func:
await message.reply("<code>Unsupported Media...</code>")
return
response = await message.reply("<code>Processing...</code>")
bot = getattr(bot, "bot", bot)
file_id, emoji = await media_func(message=replied, ff="-f" in message.flags)
try:
stickers = await kang_sticker(bot, file_id, emoji, user=message.from_user)
await response.edit(
f"Kanged: <a href='t.me/addstickers/{stickers.set.short_name}'>here</a>",
disable_preview=True,
)
except Exception as e:
await response.edit(str(e))
if not extra_config.USE_LEGACY_KANG:
BOT.add_cmd("kang")(kang)

View File

@@ -0,0 +1,216 @@
import asyncio
import os
import random
import shutil
import time
from io import BytesIO
from PIL import Image
from pyrogram import raw
from pyrogram.enums import MessageMediaType
from pyrogram.errors import StickersetInvalid
from ub_core import utils as core_utils
from app import BOT, Message, bot, extra_config
EMOJIS = ("", "🤡", "🙂", "🤔", "🔪", "😂", "💀")
async def get_sticker_set(limit: int, is_video=False) -> tuple[str, str, bool]:
count = 0
pack_name = f"PUB_{bot.me.id}_pack"
video = "_video" if is_video else ""
create_new = False
while True:
try:
sticker = await bot.invoke(
raw.functions.messages.GetStickerSet(
stickerset=raw.types.InputStickerSetShortName(
short_name=f"{pack_name}{video}_{count}"
),
hash=0,
)
)
if sticker.set.count < limit:
break
count += 1
except StickersetInvalid:
create_new = True
break
if cus_nick := os.environ.get("CUSTOM_PACK_NAME"):
pack_title = cus_nick + video
else:
pack_title = (
f"{bot.me.username or core_utils.get_name(bot.me)}'s {video}kang pack vol {count}"
)
return pack_title, f"{pack_name}{video}_{count}", create_new
async def photo_kang(message: Message, **_) -> dict:
download_path = os.path.join("downloads", str(time.time()))
os.makedirs(download_path, exist_ok=True)
input_file = os.path.join(download_path, "photo.jpg")
await message.download(input_file)
file = await asyncio.to_thread(resize_photo, input_file)
return dict(cmd="/newpack", limit=120, is_video=False, file=file, path=download_path)
def resize_photo(input_file: str) -> BytesIO:
image = Image.open(input_file)
maxsize = 512
scale = maxsize / max(image.width, image.height)
new_size = (int(image.width * scale), int(image.height * scale))
image = image.resize(new_size, Image.LANCZOS)
resized_photo = BytesIO()
resized_photo.name = "sticker.png"
image.save(resized_photo, format="PNG")
return resized_photo
async def video_kang(message: Message, ff=False) -> dict:
video = message.video or message.animation or message.document
if video.file_size > 5242880:
raise MemoryError("File Size exceeds 5MB.")
download_path = os.path.join("downloads", f"{time.time()}")
os.makedirs(download_path, exist_ok=True)
input_file = os.path.join(download_path, "input.mp4")
output_file = os.path.join(download_path, "sticker.webm")
await message.download(input_file)
if not hasattr(video, "duration"):
duration = await core_utils.get_duration(file=input_file)
else:
duration = video.duration
await resize_video(input_file=input_file, output_file=output_file, duration=duration, ff=ff)
return dict(cmd="/newvideo", limit=50, is_video=True, file=output_file, path=download_path)
async def resize_video(input_file: str, output_file: str, duration: int, ff: bool = False):
cmd = f"ffmpeg -hide_banner -loglevel error -i '{input_file}' -vf "
if ff:
cmd += '"scale=w=512:h=512:force_original_aspect_ratio=decrease,setpts=0.3*PTS" '
cmd += "-ss 0 -t 3 -r 30 -loop 0 -an -c:v libvpx-vp9 -b:v 256k -fs 256k "
elif duration < 3:
cmd += '"scale=w=512:h=512:force_original_aspect_ratio=decrease" '
cmd += "-ss 0 -r 30 -an -c:v libvpx-vp9 -b:v 256k -fs 256k "
else:
cmd += '"scale=w=512:h=512:force_original_aspect_ratio=decrease" '
cmd += "-ss 0 -t 3 -r 30 -an -c:v libvpx-vp9 -b:v 256k -fs 256k "
await core_utils.run_shell_cmd(cmd=f"{cmd}'{output_file}'")
async def document_kang(message: Message, ff: bool = False) -> dict:
name, ext = os.path.splitext(message.document.file_name)
if ext.lower() in core_utils.MediaExts.PHOTO:
return await photo_kang(message)
elif ext.lower() in {*core_utils.MediaExts.VIDEO, *core_utils.MediaExts.GIF}:
return await video_kang(message=message, ff=ff)
async def sticker_kang(message: Message, **_) -> dict:
emoji = message.sticker.emoji
sticker = message.sticker
if sticker.is_animated:
raise TypeError("Animated Stickers Not Supported.")
if sticker.is_video:
input_file: BytesIO = await message.download(in_memory=True)
input_file.seek(0)
return dict(cmd="/newvideo", emoji=emoji, is_video=True, file=input_file, limit=50)
return dict(cmd="/newpack", emoji=emoji, is_video=False, sticker=sticker, limit=120)
MEDIA_TYPE_MAP = {
MessageMediaType.PHOTO: photo_kang,
MessageMediaType.VIDEO: video_kang,
MessageMediaType.ANIMATION: video_kang,
MessageMediaType.DOCUMENT: document_kang,
MessageMediaType.STICKER: sticker_kang,
}
async def create_n_kang(kwargs: dict, pack_title: str, pack_name: str, message: Message):
async with bot.Convo(client=bot, chat_id="stickers", timeout=5) as convo:
await convo.send_message(text=kwargs["cmd"], get_response=True)
await convo.send_message(text=pack_title, get_response=True)
if kwargs.get("sticker"):
await message.reply_to_message.copy(chat_id="stickers", caption="")
await convo.get_response()
else:
await convo.send_document(document=kwargs["file"], get_response=True)
await convo.send_message(
text=kwargs.get("emoji") or random.choice(EMOJIS), get_response=True
)
await convo.send_message(text="/publish", get_response=True)
await convo.send_message("/skip")
await convo.send_message(pack_name, get_response=True)
if kwargs.get("path"):
shutil.rmtree(kwargs["path"], ignore_errors=True)
async def kang_sticker(bot: BOT, message: Message):
"""
CMD: LEGACY KANG
INFO: Save a sticker/image/gif/video to your sticker pack.
FLAGS: -f to fastforward video tp fit 3 sec duration.
USAGE: .kang | .kang -f
"""
replied = message.replied
media_func = MEDIA_TYPE_MAP.get(replied.media)
if not media_func:
await message.reply("Unsupported Media.")
return
response: Message = await message.reply("<code>Processing...</code>")
kwargs: dict = await media_func(message=replied, ff="-f" in message.flags)
pack_title, pack_name, create_new = await get_sticker_set(
limit=kwargs["limit"], is_video=kwargs["is_video"]
)
if create_new:
await create_n_kang(
kwargs=kwargs, pack_title=pack_title, pack_name=pack_name, message=message
)
await response.edit(text=f"Kanged: <a href='t.me/addstickers/{pack_name}'>here</a>")
return
async with bot.Convo(client=bot, chat_id="stickers", timeout=5) as convo:
await convo.send_message(text="/addsticker", get_response=True)
await convo.send_message(text=pack_name, get_response=True)
if kwargs.get("sticker"):
await replied.copy(chat_id="stickers", caption="")
await convo.get_response()
else:
await convo.send_document(document=kwargs["file"], get_response=True)
await convo.send_message(
text=kwargs.get("emoji") or random.choice(EMOJIS), get_response=True
)
await convo.send_message(text="/done", get_response=True)
if kwargs.get("path"):
shutil.rmtree(kwargs["path"], ignore_errors=True)
await response.edit(
text=f"Kanged: <a href='t.me/addstickers/{pack_name}'>here</a>", disable_preview=True
)
if extra_config.USE_LEGACY_KANG:
BOT.add_cmd("kang")(kang_sticker)

View File

@@ -0,0 +1,13 @@
from datetime import datetime
from app import BOT, Message
# Not my Code
# Prolly from Userge/UX/VenomX IDK
@BOT.add_cmd(cmd="ping")
async def ping_bot(bot: BOT, message: Message):
start = datetime.now()
resp: Message = await message.reply("Checking Ping.....")
end = (datetime.now() - start).microseconds / 1000
await resp.edit(f"Pong! {end} ms.")

View File

@@ -0,0 +1,223 @@
import asyncio
from collections import defaultdict
from pyrogram import filters
from pyrogram.enums import ChatType, MessageEntityType, ParseMode
from pyrogram.errors import MessageIdInvalid
from ub_core.utils.helpers import get_name
from app import BOT, Config, CustomDB, Message, bot, extra_config
LOGGER = CustomDB["COMMON_SETTINGS"]
MESSAGE_CACHE: dict[int, list[Message]] = defaultdict(list)
FLOOD_LIST: list[int] = []
async def init_task():
tag_check = await LOGGER.find_one({"_id": "tag_logger_switch"})
pm_check = await LOGGER.find_one({"_id": "pm_logger_switch"})
if tag_check:
extra_config.TAG_LOGGER = tag_check["value"]
if pm_check:
extra_config.PM_LOGGER = pm_check["value"]
Config.BACKGROUND_TASKS.append(asyncio.create_task(runner(), name="pm_tag_logger"))
@bot.add_cmd(cmd=["taglogger", "pmlogger"])
async def logger_switch(bot: BOT, message: Message):
"""
CMD: TAGLOGGER | PMLOGGER
INFO: Enable/Disable PM or Tag Logger.
FLAGS: -c to check status.
"""
text = "pm" if message.cmd == "pmlogger" else "tag"
conf_str = f"{text.upper()}_LOGGER"
if "-c" in message.flags:
await message.reply(
text=f"{text.capitalize()} Logger is enabled: <b>{getattr(extra_config, conf_str)}</b>!",
del_in=8,
)
return
value: bool = not getattr(extra_config, conf_str)
setattr(extra_config, conf_str, value)
await asyncio.gather(
LOGGER.add_data({"_id": f"{text}_logger_switch", "value": value}),
message.reply(text=f"{text.capitalize()} Logger is enabled: <b>{value}</b>!", del_in=8),
bot.log_text(text=f"#{text.capitalize()}Logger is enabled: <b>{value}</b>!", type="info"),
)
for task in Config.BACKGROUND_TASKS:
if task.get_name() == "pm_tag_logger" and task.done():
Config.BACKGROUND_TASKS.append(asyncio.create_task(runner(), name="pm_tag_logger"))
BASIC_FILTERS = (
~filters.channel
& ~filters.bot
& ~filters.service
& ~filters.chat(chats=[bot.me.id])
& ~filters.me
& ~filters.create(lambda _, __, m: m.chat.is_support)
)
@bot.on_message(
filters=BASIC_FILTERS
& filters.private
& filters.create(lambda _, __, ___: extra_config.PM_LOGGER),
)
async def pm_logger(bot: BOT, message: Message):
cache_message(message)
TAG_FILTER = filters.create(lambda _, __, ___: extra_config.TAG_LOGGER)
@bot.on_message(
filters=(BASIC_FILTERS & filters.reply & TAG_FILTER) & ~filters.private,
)
async def reply_logger(bot: BOT, message: Message):
if (
message.reply_to_message
and message.reply_to_message.from_user
and message.reply_to_message.from_user.id == bot.me.id
):
cache_message(message)
message.continue_propagation()
@bot.on_message(
filters=(BASIC_FILTERS & filters.mentioned & TAG_FILTER) & ~filters.private,
)
async def mention_logger(bot: BOT, message: Message):
for entity in message.entities or []:
if entity.type == MessageEntityType.MENTION and entity.user and entity.user.id == bot.me.id:
cache_message(message)
message.continue_propagation()
@bot.on_message(
filters=(BASIC_FILTERS & (filters.text | filters.media) & TAG_FILTER) & ~filters.private,
)
async def username_logger(bot: BOT, message: Message):
text = message.text or message.caption or ""
if bot.me.username and f"@{bot.me.username}" in text:
cache_message(message)
message.continue_propagation()
def cache_message(message: Message):
chat_id = message.chat.id
if len(MESSAGE_CACHE[chat_id]) >= 10 and chat_id not in FLOOD_LIST:
bot.log.error(f"Message not Logged from chat: {get_name(message.chat)}")
FLOOD_LIST.append(chat_id)
return
if chat_id in FLOOD_LIST:
FLOOD_LIST.remove(chat_id)
MESSAGE_CACHE[chat_id].append(message)
async def runner():
if not (extra_config.TAG_LOGGER or extra_config.PM_LOGGER):
return
last_pm_logged_id = 0
while True:
cached_keys = list(MESSAGE_CACHE.keys())
if not cached_keys:
await asyncio.sleep(5)
continue
first_key = cached_keys[0]
cached_list = MESSAGE_CACHE.copy()[first_key]
if not cached_list:
MESSAGE_CACHE.pop(first_key)
for idx, msg in enumerate(cached_list):
if msg.chat.type == ChatType.PRIVATE:
if last_pm_logged_id != first_key:
last_pm_logged_id = first_key
log_info = True
else:
log_info = False
coro = log_pm(message=msg, log_info=log_info)
else:
coro = log_chat(message=msg)
try:
await coro
except BaseException:
pass
MESSAGE_CACHE[first_key].remove(msg)
await asyncio.sleep(5)
await asyncio.sleep(15)
async def log_pm(message: Message, log_info: bool):
if log_info:
await bot.send_message(
chat_id=extra_config.MESSAGE_LOGGER_CHAT,
text=f"#PM\n{message.from_user.mention} [{message.from_user.id}]",
message_thread_id=extra_config.PM_LOGGER_THREAD_ID,
)
notice = (
f"{message.from_user.mention} [{message.from_user.id}] deleted this message."
f"\n\n---\n\n"
f"Message: \n<a href='{message.link}'>{message.chat.title or message.chat.first_name}</a> ({message.chat.id})"
f"\n\n---\n\n"
f"Caption:\n{message.caption or 'No Caption in media.'}"
)
await log_message(message=message, notice=notice, thread_id=extra_config.PM_LOGGER_THREAD_ID)
async def log_chat(message: Message):
if message.sender_chat:
mention, u_id = message.sender_chat.title, message.sender_chat.id
else:
mention, u_id = message.from_user.mention, message.from_user.id
notice = (
f"{mention} [{u_id}] deleted this message."
f"\n\n---\n\n"
f"Message: \n<a href='{message.link}'>{message.chat.title or message.chat.first_name}</a> ({message.chat.id})"
f"\n\n---\n\n"
f"Caption:\n{message.caption or 'No Caption in media.'}"
)
if message.reply_to_message:
await log_message(message.reply_to_message, thread_id=extra_config.TAG_LOGGER_THREAD_ID)
await log_message(
message=message,
notice=notice,
extra_info=f"#TAG\n{mention} [{u_id}]\nMessage: \n<a href='{message.link}'>{message.chat.title}</a> ({message.chat.id})",
thread_id=extra_config.TAG_LOGGER_THREAD_ID,
)
async def log_message(
message: Message,
notice: str | None = None,
extra_info: str | None = None,
thread_id: int = None,
):
try:
logged_message: Message = await message.forward(
extra_config.MESSAGE_LOGGER_CHAT, message_thread_id=thread_id
)
if extra_info:
await logged_message.reply(extra_info, parse_mode=ParseMode.HTML)
except MessageIdInvalid:
logged_message = await message.copy(
extra_config.MESSAGE_LOGGER_CHAT, message_thread_id=thread_id
)
if notice:
await logged_message.reply(notice, parse_mode=ParseMode.HTML)

View File

@@ -0,0 +1,153 @@
import asyncio
from collections import defaultdict
from pyrogram import filters
from pyrogram.enums import ChatType
from ub_core.utils.helpers import get_name
from app import BOT, CustomDB, Message, bot, extra_config
PM_USERS = CustomDB["PM_USERS"]
PM_GUARD = CustomDB["COMMON_SETTINGS"]
ALLOWED_USERS: list[int] = []
RECENT_USERS: dict = defaultdict(int)
async def init_task():
guard = (await PM_GUARD.find_one({"_id": "guard_switch"})) or {}
extra_config.PM_GUARD = guard.get("value", False)
[ALLOWED_USERS.append(user_id["_id"]) async for user_id in PM_USERS.find()]
async def pm_permit_filter(_, __, message: Message):
# Return False if:
if (
# PM_GUARD is False
not extra_config.PM_GUARD
# Chat is not Private
or message.chat.type != ChatType.PRIVATE
# Chat is already approved
or message.chat.id in ALLOWED_USERS
# Saved Messages
or message.chat.id == bot.me.id
# PM is BOT
or message.from_user.is_bot
# Telegram Service Messages like OTPs.
or message.from_user.is_support
# Chat Service Messages like pinned a pic etc
or message.service
):
return False
return True
PERMIT_FILTER = filters.create(pm_permit_filter)
@bot.on_message(PERMIT_FILTER & filters.incoming, group=0)
async def handle_new_pm(bot: BOT, message: Message):
user_id = message.from_user.id
if RECENT_USERS[user_id] == 0:
await bot.log_text(
text=f"#PMGUARD\n{message.from_user.mention} [{user_id}] has messaged you.", type="info"
)
RECENT_USERS[user_id] += 1
if message.chat.is_support:
return
if RECENT_USERS[user_id] >= 5:
await message.reply("You've been blocked for spamming.")
await bot.block_user(user_id)
RECENT_USERS.pop(user_id)
await bot.log_text(
text=f"#PMGUARD\n{message.from_user.mention} [{user_id}] has been blocked for spamming.",
type="info",
)
return
if RECENT_USERS[user_id] % 2:
await message.reply("You are not authorised to PM.")
@bot.on_message(PERMIT_FILTER & filters.outgoing, group=2)
async def auto_approve(bot: BOT, message: Message):
message = Message(message=message)
ALLOWED_USERS.append(message.chat.id)
await asyncio.gather(
PM_USERS.insert_one({"_id": message.chat.id}),
message.reply(text="Auto-Approved to PM.", del_in=5),
)
@bot.add_cmd(cmd="pmguard")
async def pm_guard(bot: BOT, message: Message):
"""
CMD: PMGUARD
INFO: Enable/Disable PM GUARD.
FLAGS: -c to check guard status.
USAGE:
.pmguard | .pmguard -c
"""
if "-c" in message.flags:
await message.reply(text=f"PM Guard is enabled: <b>{extra_config.PM_GUARD}</b>", del_in=8)
return
value = not extra_config.PM_GUARD
extra_config.PM_GUARD = value
await asyncio.gather(
PM_GUARD.add_data({"_id": "guard_switch", "value": value}),
message.reply(text=f"PM Guard is enabled: <b>{value}</b>!", del_in=8),
)
@bot.add_cmd(cmd=["a", "allow"])
async def allow_pm(bot: BOT, message: Message):
"""
CMD: A | ALLOW
INFO: Approve a User to PM.
USAGE: .a|.allow [reply to a user or in pm]
"""
user_id, name = get_userID_name(message)
if not user_id:
await message.reply(
"Unable to extract User to allow.\n<code>Give user id | Reply to a user | use in PM.</code>"
)
return
if user_id in ALLOWED_USERS:
await message.reply(f"{name} is already approved.")
return
ALLOWED_USERS.append(user_id)
RECENT_USERS.pop(user_id, 0)
await asyncio.gather(
message.reply(text=f"{name} allowed to PM.", del_in=8),
PM_USERS.insert_one({"_id": user_id}),
)
@bot.add_cmd(cmd="nopm")
async def no_pm(bot: BOT, message: Message):
user_id, name = get_userID_name(message)
if not user_id:
await message.reply(
"Unable to extract User to Dis-allow.\n<code>Give user id | Reply to a user | use in PM.</code>"
)
return
if user_id not in ALLOWED_USERS:
await message.reply(f"{name} is not approved to PM.")
return
ALLOWED_USERS.remove(user_id)
await asyncio.gather(
message.reply(text=f"{name} Dis-allowed to PM.", del_in=8), PM_USERS.delete_data(user_id)
)
def get_userID_name(message: Message) -> tuple:
if message.filtered_input and message.filtered_input.isdigit():
user_id = int(message.filtered_input)
return user_id, user_id
elif message.replied:
return message.replied.from_user.id, get_name(message.replied.from_user)
elif message.chat.type == ChatType.PRIVATE:
return message.chat.id, get_name(message.chat)
else:
return 0, 0

View File

@@ -0,0 +1,33 @@
from app import BOT, Message
from app.plugins.tg_tools.get_message import parse_link
@BOT.add_cmd(cmd="reply")
async def reply(bot: BOT, message: Message) -> None:
"""
CMD: REPLY
INFO: Reply to a Message.
FLAGS:
-r: reply remotely using message link.
USAGE:
.reply HI | .reply -r t.me/... HI
"""
if "-r" in message.flags:
input: list[str] = message.filtered_input.split(" ", maxsplit=1)
if len(input) < 2:
await message.reply("The '-r' flag requires a message link and text.")
return
message_link, text = input
chat_id, _, reply_to_id = parse_link(message_link.strip())
else:
chat_id, text, reply_to_id = message.chat.id, message.input, message.reply_id
if not text:
return
await bot.send_message(
chat_id=chat_id, text=text, reply_to_id=reply_to_id, disable_preview=True
)

View File

@@ -0,0 +1,27 @@
import re
from app import BOT, Message
@BOT.add_cmd(cmd="resp")
async def respond(bot: BOT, message: Message):
"""
CMD: RESP
INFO: Respond to a Logged Message.
USAGE:
.resp [chat_id | reply to a message containing info] hi
"""
if message.replied:
inp_text = message.replied.text
pattern = r"\((-\d+)\)" if "#TAG" in inp_text else r"\[(\d+)\]"
match = re.search(pattern=pattern, string=inp_text)
if match:
chat_id = match.group(1)
text = message.input
elif message.input:
chat_id, text = message.input.split(" ", maxsplit=1)
else:
await message.reply("Unable to extract chat_id and text.")
return
await bot.send_message(chat_id=int(chat_id), text=text, disable_preview=True)

263
app/plugins/utils/calc.py Normal file
View File

@@ -0,0 +1,263 @@
import math
import re
from decimal import Decimal
from app import BOT, Message
@BOT.add_cmd(cmd="calc")
async def calculator(bot: BOT, message: Message):
"""
CMD: CALC
INFO: Advanced calculator with support for mathematical functions.
USAGE: .calc 2 + 2 * 3
.calc sqrt(16) + pi
.calc sin(30 * pi / 180)
"""
expression = message.filtered_input
if not expression:
await message.reply("❌ <b>No expression provided.</b>\n"
"Usage: <code>.calc [expression]</code>\n"
"Examples:\n"
"• <code>.calc 2 + 2 * 3</code>\n"
"• <code>.calc sqrt(16) + pi</code>\n"
"• <code>.calc sin(45 * pi / 180)</code>")
return
response = await message.reply(f"🧮 <b>Calculating...</b>\n<code>{expression}</code>")
try:
# Prepare safe evaluation environment
safe_dict = {
# Basic math functions
'abs': abs, 'round': round, 'min': min, 'max': max,
'sum': sum, 'pow': pow,
# Math constants
'pi': math.pi, 'e': math.e, 'tau': math.tau,
'inf': math.inf, 'nan': math.nan,
# Trigonometric functions
'sin': math.sin, 'cos': math.cos, 'tan': math.tan,
'asin': math.asin, 'acos': math.acos, 'atan': math.atan,
'atan2': math.atan2,
# Hyperbolic functions
'sinh': math.sinh, 'cosh': math.cosh, 'tanh': math.tanh,
'asinh': math.asinh, 'acosh': math.acosh, 'atanh': math.atanh,
# Exponential and logarithmic
'exp': math.exp, 'log': math.log, 'log10': math.log10,
'log2': math.log2, 'sqrt': math.sqrt,
# Other functions
'ceil': math.ceil, 'floor': math.floor,
'factorial': math.factorial, 'degrees': math.degrees,
'radians': math.radians, 'gcd': math.gcd,
# Constants for convenience
'__builtins__': {} # Remove access to builtins for security
}
# Replace common mathematical notation
expression = expression.replace('^', '**') # Power operator
expression = expression.replace('×', '*') # Multiplication
expression = expression.replace('÷', '/') # Division
# Evaluate the expression
result = eval(expression, safe_dict)
# Format the result
if isinstance(result, float):
if result.is_integer():
result_str = str(int(result))
else:
# Round to reasonable precision
result_str = f"{result:.10g}"
else:
result_str = str(result)
# Create result message
calc_text = f"🧮 <b>Calculator Result</b>\n\n"
calc_text += f"<b>Expression:</b> <code>{message.filtered_input}</code>\n"
calc_text += f"<b>Result:</b> <code>{result_str}</code>\n"
# Add additional info for special values
if isinstance(result, float):
if result == math.pi:
calc_text += f"<b>Note:</b> <i>This is π (pi)</i>\n"
elif result == math.e:
calc_text += f"<b>Note:</b> <i>This is e (Euler's number)</i>\n"
elif math.isinf(result):
calc_text += f"<b>Note:</b> <i>Result is infinity</i>\n"
elif math.isnan(result):
calc_text += f"<b>Note:</b> <i>Result is not a number (NaN)</i>\n"
calc_text += "\n✅ <b>Calculation completed!</b>"
await response.edit(calc_text)
except ZeroDivisionError:
await response.edit("❌ <b>Division by zero error!</b>\n"
f"Expression: <code>{expression}</code>")
except (ValueError, TypeError) as e:
await response.edit(f"❌ <b>Invalid expression!</b>\n"
f"Expression: <code>{expression}</code>\n"
f"Error: <code>{str(e)}</code>")
except Exception as e:
await response.edit(f"❌ <b>Calculation error!</b>\n"
f"Expression: <code>{expression}</code>\n"
f"Error: <code>{str(e)}</code>")
@BOT.add_cmd(cmd="convert")
async def unit_converter(bot: BOT, message: Message):
"""
CMD: CONVERT
INFO: Convert between different units (temperature, length, weight, etc.).
USAGE: .convert 100 c f (Celsius to Fahrenheit)
.convert 5 km mi (Kilometers to Miles)
.convert 10 kg lb (Kilograms to Pounds)
"""
args = message.filtered_input.split()
if len(args) != 3:
await message.reply("❌ <b>Invalid format!</b>\n"
"Usage: <code>.convert [value] [from_unit] [to_unit]</code>\n\n"
"<b>Supported conversions:</b>\n"
"• Temperature: c, f, k (Celsius, Fahrenheit, Kelvin)\n"
"• Length: m, km, ft, mi, in, cm, mm\n"
"• Weight: kg, g, lb, oz\n"
"• Area: m2, km2, ft2, in2, acre\n"
"• Volume: l, ml, gal, qt, pt, cup")
return
try:
value = float(args[0])
from_unit = args[1].lower()
to_unit = args[2].lower()
except ValueError:
await message.reply("❌ <b>Invalid value!</b> Please provide a numeric value.")
return
response = await message.reply(f"🔄 <b>Converting {value} {from_unit} to {to_unit}...</b>")
try:
result = None
conversion_type = None
# Temperature conversions
if from_unit in ['c', 'f', 'k'] and to_unit in ['c', 'f', 'k']:
conversion_type = "Temperature"
# Convert to Celsius first
if from_unit == 'f':
celsius = (value - 32) * 5/9
elif from_unit == 'k':
celsius = value - 273.15
else:
celsius = value
# Convert from Celsius to target
if to_unit == 'f':
result = celsius * 9/5 + 32
elif to_unit == 'k':
result = celsius + 273.15
else:
result = celsius
# Length conversions (convert to meters first)
elif from_unit in ['m', 'km', 'ft', 'mi', 'in', 'cm', 'mm'] and to_unit in ['m', 'km', 'ft', 'mi', 'in', 'cm', 'mm']:
conversion_type = "Length"
# To meters
to_meters = {
'm': 1, 'km': 1000, 'cm': 0.01, 'mm': 0.001,
'ft': 0.3048, 'in': 0.0254, 'mi': 1609.34
}
meters = value * to_meters[from_unit]
result = meters / to_meters[to_unit]
# Weight conversions (convert to grams first)
elif from_unit in ['kg', 'g', 'lb', 'oz'] and to_unit in ['kg', 'g', 'lb', 'oz']:
conversion_type = "Weight"
# To grams
to_grams = {
'g': 1, 'kg': 1000, 'lb': 453.592, 'oz': 28.3495
}
grams = value * to_grams[from_unit]
result = grams / to_grams[to_unit]
else:
await response.edit("❌ <b>Unsupported conversion!</b>\n"
"Please check the supported units in the help message.")
return
# Format result
if result.is_integer():
result_str = str(int(result))
else:
result_str = f"{result:.6g}"
# Unit names for display
unit_names = {
'c': '°C', 'f': '°F', 'k': 'K',
'm': 'meters', 'km': 'kilometers', 'cm': 'centimeters', 'mm': 'millimeters',
'ft': 'feet', 'in': 'inches', 'mi': 'miles',
'kg': 'kilograms', 'g': 'grams', 'lb': 'pounds', 'oz': 'ounces'
}
from_name = unit_names.get(from_unit, from_unit)
to_name = unit_names.get(to_unit, to_unit)
convert_text = f"🔄 <b>Unit Conversion Result</b>\n\n"
convert_text += f"<b>Type:</b> {conversion_type}\n"
convert_text += f"<b>From:</b> {value} {from_name}\n"
convert_text += f"<b>To:</b> {result_str} {to_name}\n"
convert_text += "\n✅ <b>Conversion completed!</b>"
await response.edit(convert_text)
except Exception as e:
await response.edit(f"❌ <b>Conversion error!</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="math")
async def math_help(bot: BOT, message: Message):
"""
CMD: MATH
INFO: Show available mathematical functions and constants.
USAGE: .math
"""
help_text = """🧮 <b>Mathematical Functions & Constants</b>
<b>📐 Basic Operations:</b>
<code>+, -, *, /, **, %, //</code>
<b>🔢 Functions:</b>
<code>abs(), round(), min(), max(), sum(), pow()</code>
<b>📊 Constants:</b>
<code>pi, e, tau, inf, nan</code>
<b>📐 Trigonometric:</b>
<code>sin(), cos(), tan(), asin(), acos(), atan()</code>
<b>📈 Exponential & Logarithmic:</b>
<code>exp(), log(), log10(), log2(), sqrt()</code>
<b>🔄 Other Functions:</b>
<code>ceil(), floor(), factorial(), degrees(), radians()</code>
<b>🌡️ Unit Conversions:</b>
Use <code>.convert</code> for temperature, length, weight conversions
<b>💡 Examples:</b>
• <code>.calc 2**8</code> - Calculate 2 to the power of 8
• <code>.calc sin(pi/2)</code> - Sine of 90 degrees
• <code>.calc sqrt(144) + log10(100)</code> - Complex expression
• <code>.convert 100 c f</code> - Convert 100°C to Fahrenheit"""
await message.reply(help_text)

278
app/plugins/utils/encode.py Normal file
View File

@@ -0,0 +1,278 @@
import base64
import hashlib
import urllib.parse
import binascii
import json
from html import escape, unescape
from app import BOT, Message
@BOT.add_cmd(cmd="encode")
async def encode_text(bot: BOT, message: Message):
"""
CMD: ENCODE
INFO: Encode text using various encoding methods.
FLAGS: -b64 (base64), -url (URL encode), -hex (hexadecimal), -html (HTML entities)
USAGE: .encode -b64 Hello World
.encode -url Hello World!
.encode -hex Secret Text
"""
text = message.filtered_input
if not text:
await message.reply("❌ <b>No text provided!</b>\n"
"Usage: <code>.encode -[method] [text]</code>\n\n"
"<b>Available methods:</b>\n"
"• <code>-b64</code> - Base64 encoding\n"
"• <code>-url</code> - URL encoding\n"
"• <code>-hex</code> - Hexadecimal encoding\n"
"• <code>-html</code> - HTML entities encoding")
return
response = await message.reply("🔄 <b>Encoding text...</b>")
try:
results = []
# Base64 encoding
if "-b64" in message.flags:
encoded = base64.b64encode(text.encode('utf-8')).decode('utf-8')
results.append(f"<b>📦 Base64:</b>\n<code>{encoded}</code>")
# URL encoding
if "-url" in message.flags:
encoded = urllib.parse.quote(text, safe='')
results.append(f"<b>🌐 URL Encoded:</b>\n<code>{encoded}</code>")
# Hexadecimal encoding
if "-hex" in message.flags:
encoded = text.encode('utf-8').hex()
results.append(f"<b>🔢 Hexadecimal:</b>\n<code>{encoded}</code>")
# HTML entities encoding
if "-html" in message.flags:
encoded = escape(text)
results.append(f"<b>🌐 HTML Entities:</b>\n<code>{encoded}</code>")
if not results:
# Default to base64 if no method specified
encoded = base64.b64encode(text.encode('utf-8')).decode('utf-8')
results.append(f"<b>📦 Base64 (default):</b>\n<code>{encoded}</code>")
encode_text = f"🔐 <b>Text Encoding Results</b>\n\n"
encode_text += f"<b>Original Text:</b>\n<code>{text}</code>\n\n"
encode_text += "\n\n".join(results)
encode_text += "\n\n✅ <b>Encoding completed!</b>"
await response.edit(encode_text)
except Exception as e:
await response.edit(f"❌ <b>Encoding error:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="decode")
async def decode_text(bot: BOT, message: Message):
"""
CMD: DECODE
INFO: Decode text using various decoding methods.
FLAGS: -b64 (base64), -url (URL decode), -hex (hexadecimal), -html (HTML entities)
USAGE: .decode -b64 SGVsbG8gV29ybGQ=
.decode -url Hello%20World%21
.decode -hex 48656c6c6f20576f726c64
"""
text = message.filtered_input
if not text:
await message.reply("❌ <b>No text provided!</b>\n"
"Usage: <code>.decode -[method] [encoded_text]</code>\n\n"
"<b>Available methods:</b>\n"
"• <code>-b64</code> - Base64 decoding\n"
"• <code>-url</code> - URL decoding\n"
"• <code>-hex</code> - Hexadecimal decoding\n"
"• <code>-html</code> - HTML entities decoding")
return
response = await message.reply("🔄 <b>Decoding text...</b>")
try:
results = []
# Base64 decoding
if "-b64" in message.flags:
try:
decoded = base64.b64decode(text).decode('utf-8')
results.append(f"<b>📦 Base64 Decoded:</b>\n<code>{decoded}</code>")
except Exception as e:
results.append(f"<b>📦 Base64:</b> ❌ <code>{str(e)}</code>")
# URL decoding
if "-url" in message.flags:
try:
decoded = urllib.parse.unquote(text)
results.append(f"<b>🌐 URL Decoded:</b>\n<code>{decoded}</code>")
except Exception as e:
results.append(f"<b>🌐 URL:</b> ❌ <code>{str(e)}</code>")
# Hexadecimal decoding
if "-hex" in message.flags:
try:
decoded = bytes.fromhex(text).decode('utf-8')
results.append(f"<b>🔢 Hexadecimal Decoded:</b>\n<code>{decoded}</code>")
except Exception as e:
results.append(f"<b>🔢 Hexadecimal:</b> ❌ <code>{str(e)}</code>")
# HTML entities decoding
if "-html" in message.flags:
try:
decoded = unescape(text)
results.append(f"<b>🌐 HTML Entities Decoded:</b>\n<code>{decoded}</code>")
except Exception as e:
results.append(f"<b>🌐 HTML:</b> ❌ <code>{str(e)}</code>")
if not results:
# Try to auto-detect and decode
# Try base64 first
try:
decoded = base64.b64decode(text).decode('utf-8')
results.append(f"<b>📦 Auto-detected Base64:</b>\n<code>{decoded}</code>")
except:
# Try hex
try:
decoded = bytes.fromhex(text).decode('utf-8')
results.append(f"<b>🔢 Auto-detected Hexadecimal:</b>\n<code>{decoded}</code>")
except:
# Try URL decode
try:
decoded = urllib.parse.unquote(text)
if decoded != text: # Only show if actually decoded something
results.append(f"<b>🌐 Auto-detected URL:</b>\n<code>{decoded}</code>")
else:
results.append("❌ Could not auto-detect encoding method.")
except:
results.append("❌ Could not auto-detect encoding method.")
decode_text = f"🔓 <b>Text Decoding Results</b>\n\n"
decode_text += f"<b>Encoded Text:</b>\n<code>{text}</code>\n\n"
decode_text += "\n\n".join(results)
decode_text += "\n\n✅ <b>Decoding completed!</b>"
await response.edit(decode_text)
except Exception as e:
await response.edit(f"❌ <b>Decoding error:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="hash")
async def hash_text(bot: BOT, message: Message):
"""
CMD: HASH
INFO: Generate various hash values for text.
FLAGS: -md5, -sha1, -sha256, -sha512, -all (for all hashes)
USAGE: .hash -sha256 Hello World
.hash -all Secret Text
"""
text = message.filtered_input
if not text:
await message.reply("❌ <b>No text provided!</b>\n"
"Usage: <code>.hash -[method] [text]</code>\n\n"
"<b>Available methods:</b>\n"
"• <code>-md5</code> - MD5 hash\n"
"• <code>-sha1</code> - SHA1 hash\n"
"• <code>-sha256</code> - SHA256 hash\n"
"• <code>-sha512</code> - SHA512 hash\n"
"• <code>-all</code> - All hash methods")
return
response = await message.reply("🔄 <b>Generating hashes...</b>")
try:
text_bytes = text.encode('utf-8')
results = []
# Generate specific hashes based on flags
if "-md5" in message.flags or "-all" in message.flags:
md5_hash = hashlib.md5(text_bytes).hexdigest()
results.append(f"<b>🔐 MD5:</b>\n<code>{md5_hash}</code>")
if "-sha1" in message.flags or "-all" in message.flags:
sha1_hash = hashlib.sha1(text_bytes).hexdigest()
results.append(f"<b>🔐 SHA1:</b>\n<code>{sha1_hash}</code>")
if "-sha256" in message.flags or "-all" in message.flags:
sha256_hash = hashlib.sha256(text_bytes).hexdigest()
results.append(f"<b>🔐 SHA256:</b>\n<code>{sha256_hash}</code>")
if "-sha512" in message.flags or "-all" in message.flags:
sha512_hash = hashlib.sha512(text_bytes).hexdigest()
results.append(f"<b>🔐 SHA512:</b>\n<code>{sha512_hash}</code>")
if not results:
# Default to SHA256 if no method specified
sha256_hash = hashlib.sha256(text_bytes).hexdigest()
results.append(f"<b>🔐 SHA256 (default):</b>\n<code>{sha256_hash}</code>")
hash_text = f"🔐 <b>Hash Generation Results</b>\n\n"
hash_text += f"<b>Original Text:</b>\n<code>{text}</code>\n\n"
hash_text += "\n\n".join(results)
hash_text += "\n\n✅ <b>Hash generation completed!</b>"
await response.edit(hash_text)
except Exception as e:
await response.edit(f"❌ <b>Hash generation error:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="json")
async def json_formatter(bot: BOT, message: Message):
"""
CMD: JSON
INFO: Format, validate and minify JSON data.
FLAGS: -pretty (format), -minify (minify), -validate (validate only)
USAGE: .json -pretty {"name":"John","age":30}
.json -minify { "formatted": "json" }
"""
json_text = message.filtered_input
if not json_text:
await message.reply("❌ <b>No JSON provided!</b>\n"
"Usage: <code>.json -[method] [json_data]</code>\n\n"
"<b>Available methods:</b>\n"
"• <code>-pretty</code> - Format JSON with indentation\n"
"• <code>-minify</code> - Minify JSON (remove whitespace)\n"
"• <code>-validate</code> - Validate JSON syntax only")
return
response = await message.reply("🔄 <b>Processing JSON...</b>")
try:
# Parse JSON to validate
parsed_json = json.loads(json_text)
results = []
if "-pretty" in message.flags:
pretty_json = json.dumps(parsed_json, indent=2, ensure_ascii=False)
results.append(f"<b>📋 Pretty Formatted:</b>\n<pre>{pretty_json}</pre>")
if "-minify" in message.flags:
minified_json = json.dumps(parsed_json, separators=(',', ':'), ensure_ascii=False)
results.append(f"<b>🗜️ Minified:</b>\n<code>{minified_json}</code>")
if "-validate" in message.flags:
results.append("✅ <b>JSON is valid!</b>")
if not results:
# Default to pretty format
pretty_json = json.dumps(parsed_json, indent=2, ensure_ascii=False)
results.append(f"<b>📋 Pretty Formatted (default):</b>\n<pre>{pretty_json}</pre>")
json_result = f"📋 <b>JSON Processing Results</b>\n\n"
json_result += "\n\n".join(results)
json_result += "\n\n✅ <b>JSON processing completed!</b>"
await response.edit(json_result)
except json.JSONDecodeError as e:
await response.edit(f"❌ <b>Invalid JSON!</b>\n"
f"<b>Error:</b> <code>{str(e)}</code>\n"
f"<b>Input:</b> <code>{json_text}</code>")
except Exception as e:
await response.edit(f"❌ <b>JSON processing error:</b>\n<code>{str(e)}</code>")

186
app/plugins/utils/qr.py Normal file
View File

@@ -0,0 +1,186 @@
import io
import qrcode
from PIL import Image
from app import BOT, Message
@BOT.add_cmd(cmd="qr")
async def generate_qr_code(bot: BOT, message: Message):
"""
CMD: QR
INFO: Generate QR code from text or URL.
FLAGS: -s [size] for custom size (default: 10), -b [border] for border size
USAGE: .qr https://example.com
.qr -s 15 -b 2 Hello World!
"""
text = message.filtered_input
if not text:
await message.reply("❌ <b>No text provided!</b>\n"
"Usage: <code>.qr [text/url]</code>\n"
"Example: <code>.qr https://telegram.org</code>")
return
response = await message.reply("🔄 <b>Generating QR code...</b>")
try:
# Parse flags for customization
box_size = 10 # Default size
border = 4 # Default border
if "-s" in message.flags:
try:
size_index = message.flags.index("-s") + 1
if size_index < len(message.flags):
box_size = int(message.flags[size_index])
except (ValueError, IndexError):
pass
if "-b" in message.flags:
try:
border_index = message.flags.index("-b") + 1
if border_index < len(message.flags):
border = int(message.flags[border_index])
except (ValueError, IndexError):
pass
# Generate QR code
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=box_size,
border=border,
)
qr.add_data(text)
qr.make(fit=True)
# Create image
img = qr.make_image(fill_color="black", back_color="white")
# Convert to bytes
img_bytes = io.BytesIO()
img.save(img_bytes, format='PNG')
img_bytes.seek(0)
# Send QR code image
caption = f"📱 <b>QR Code Generated</b>\n\n"
caption += f"<b>Content:</b> <code>{text}</code>\n"
caption += f"<b>Size:</b> {img.size[0]}x{img.size[1]} pixels\n"
caption += f"<b>Box Size:</b> {box_size}\n"
caption += f"<b>Border:</b> {border}"
await response.delete()
await message.reply_photo(
photo=img_bytes,
caption=caption
)
except Exception as e:
await response.edit(f"❌ <b>Error generating QR code:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="barcode")
async def generate_barcode(bot: BOT, message: Message):
"""
CMD: BARCODE
INFO: Generate a simple text-based barcode representation.
USAGE: .barcode 1234567890
"""
text = message.filtered_input
if not text:
await message.reply("❌ <b>No text provided!</b>\n"
"Usage: <code>.barcode [text/numbers]</code>\n"
"Example: <code>.barcode 1234567890</code>")
return
response = await message.reply("🔄 <b>Generating barcode...</b>")
try:
# Simple ASCII barcode representation
# This is a basic representation, not a real barcode standard
# Create a pattern based on the text
barcode_lines = []
# Header
barcode_lines.append("" * (len(text) * 8 + 4))
barcode_lines.append("" + " " * (len(text) * 8 + 2) + "")
# Generate pattern for each character
for char in text:
ascii_val = ord(char)
# Create a simple pattern based on ASCII value
pattern = ""
for i in range(8):
if (ascii_val >> i) & 1:
pattern += ""
else:
pattern += " "
barcode_lines.append("" + pattern + "")
# Footer
barcode_lines.append("" + " " * (len(text) * 8 + 2) + "")
barcode_lines.append("" * (len(text) * 8 + 4))
# Number display
number_line = " "
for char in text:
number_line += f"{char:<8}"
barcode_lines.append(number_line)
barcode_display = "\n".join(barcode_lines)
barcode_text = f"📊 <b>Text Barcode Generated</b>\n\n"
barcode_text += f"<b>Content:</b> <code>{text}</code>\n"
barcode_text += f"<b>Length:</b> {len(text)} characters\n\n"
barcode_text += f"<pre>{barcode_display}</pre>\n\n"
barcode_text += "<i>📝 This is a simple text representation, not a standard barcode format.</i>"
await response.edit(barcode_text)
except Exception as e:
await response.edit(f"❌ <b>Error generating barcode:</b>\n<code>{str(e)}</code>")
@BOT.add_cmd(cmd="qrinfo")
async def qr_code_info(bot: BOT, message: Message):
"""
CMD: QRINFO
INFO: Show information about QR code generation and usage.
USAGE: .qrinfo
"""
info_text = """📱 <b>QR Code Generator Information</b>
<b>🔧 Available Commands:</b>
• <code>.qr [text]</code> - Generate QR code from text
• <code>.qr -s [size] [text]</code> - Custom box size (1-50)
• <code>.qr -b [border] [text]</code> - Custom border size (1-20)
<b>📊 QR Code Features:</b>
• Supports text, URLs, phone numbers, email
• Error correction level: Low (faster generation)
• Output format: PNG image
• Black and white design
<b>💡 Usage Examples:</b>
• <code>.qr https://telegram.org</code>
• <code>.qr -s 15 My Secret Message</code>
• <code>.qr -s 8 -b 2 tel:+1234567890</code>
• <code>.qr mailto:user@example.com</code>
<b>📏 Size Guidelines:</b>
• Small: 5-8 (faster, lower quality)
• Medium: 10-12 (default, balanced)
• Large: 15-20 (slower, higher quality)
<b>🎯 Best Practices:</b>
• Keep text short for better scanning
• Test QR codes before sharing
• Use higher sizes for small text
• URLs work best with QR codes
<b>📊 Barcode Command:</b>
• <code>.barcode [text]</code> - Simple ASCII barcode representation</b>"""
await message.reply(info_text)

BIN
assets/dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

39
docker_start.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
export PIP_ROOT_USER_ACTION=ignore
if [ -d "ubot" ]; then
rm -rf ubot
echo "removed older ubot dir"
fi
run_extra_boot_scripts() {
local directory="scripts"
if [[ -d "$directory" ]]; then
if [[ -n "$(ls -A "$directory")" ]]; then
for file in "$directory"/*; do
if [[ -f "$file" && -x "$file" ]]; then
echo "Executing $file"
"$file"
fi
done
fi
fi
}
echo "${GH_TOKEN}" > ~/.git-credentials
git config --global credential.helper store
git clone -q --depth=1 "${UPSTREAM_REPO:-"https://github.com/thedragonsinn/plain-ub"}" ubot
cd ubot
pip -q install --no-cache-dir -r req*.txt
run_extra_boot_scripts
bash run

15
install_service.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Copy service file to systemd directory
sudo cp userbot.service /etc/systemd/system/
# Reload systemd to recognize the new service
sudo systemctl daemon-reload
# Enable the service to start on boot
sudo systemctl enable userbot.service
echo "Service installed successfully!"
echo "To start the service: sudo systemctl start userbot"
echo "To check status: sudo systemctl status userbot"
echo "To view logs: sudo journalctl -u userbot -f"

14
req.txt Normal file
View File

@@ -0,0 +1,14 @@
# BoilerPlate Code for UB
# git+https://github.com/thedragonsinn/ub-core.git
# moved to scripts/install_ub_core.sh
yt-dlp>=2024.5.27
pillow
openai
aiohttp
python-libtorrent
google-auth-oauthlib
google-auth-httplib2
google-api-python-client
google-genai

11
run Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
if ! [ -d ".git" ] ; then
git init
fi
while true; do
python -m app
exit_code=$?
[ $exit_code -ne 69 ] && break
done

88
sample-config.env Normal file
View File

@@ -0,0 +1,88 @@
API_ID=
API_HASH=
# API_PORT=
# To pass Health checks of Koyeb, Render and other hosts.
# Use the port listed in your app configuration.
CMD_TRIGGER=.
#CUSTOM_PACK_NAME=
# to set a different pack title when kanging.
# USE_LEGACY_KANG=1
# Uncomment to use old kang method that kangs to separate packs.
DEV_MODE=0
# py, sh, shell commands
DB_URL=
# Mongo DB cluster URL
# DRIVE_ROOT_ID =
# ID of the default working dir for bot in google drive
# ID can be found by copying the link of the folder
# The random string of characters after folder/ is ID
# EXTRA_MODULES_REPO=
# To add extra modules or mini bots that require stuff in ub.
# Only For Advance Users.
# FBAN_LOG_CHANNEL=
# Optional FedBan Proof and logs.
# FBAN_SUDO_ID=
# FBAN_SUDO_TRIGGER=
# Optional sudo fban vars to initiate ban in 2nd user-bot.
# GEMINI_API_KEY=
# Optional API Key
# Get from https://ai.google.dev/
LOG_CHAT=
# Bot logs chat/channel
# LOG_CHAT_THREAD_ID=
# if you want to log to a specific topic.
# MESSAGE_LOGGER_CHAT=
# For PM and Tag logger
# Defaults to sending in Log Channel Above.
# PM_LOGGER_THREAD_ID=
# TAG_LOGGER_THREAD_ID=
# Extra customisation for separated logging.
# can be used with the var above or directly with log chat.
OWNER_ID=
# Your user ID
SESSION_STRING=
# Your string session
SUDO_TRIGGER=!
# Sudo Trigger for bot
UPSTREAM_REPO=https://github.com/thedragonsinn/plain-ub
# Keep default unless you maintain your own fork.

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
[ -z "$EXTRA_MODULES_REPO" ] && { echo "EXTRA_MODULES_REPO not set, Skipping..."; exit; }
repo_name=$(basename "$EXTRA_MODULES_REPO")
echo "Installing ${repo_name} to app/modules"
git clone -q "$EXTRA_MODULES_REPO" "app/modules" || { echo "Failed to clone external repo"; exit; }
pip -q install --no-cache-dir -r app/modules/req*.txt
echo "Done"

11
scripts/install_termux_reqs.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
grep -qi "com.termux" <<< "$PATH" || { echo "Not a termux Env, Skipping..."; exit; }
mkdir -p "${HOME}/.config/pip" > /dev/null 2>&1
echo -e '[global]\nextra-index-url = https://termux-user-repository.github.io/pypi/' > "${HOME}/.config/pip/pip.conf"
./scripts/install_ub_core.sh
grep -Ev "^#|openai" req.txt | xargs -n 1 pip install

7
scripts/install_ub_core.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
dual_mode_arg=""
[ -n "$USE_DUAL_BRANCH" ] && dual_mode_arg="@dual_mode"
pip -q install --no-cache-dir --force-reinstall "git+https://github.com/thedragonsinn/ub-core${dual_mode_arg}"

1
ubot Submodule

Submodule ubot added at e0a8b22b2e

15
userbot.service Normal file
View File

@@ -0,0 +1,15 @@
[Unit]
Description=Telegram Userbot
After=network.target
[Service]
Type=simple
User=wiktoro
WorkingDirectory=/home/wiktoro/Pobrane/plain-ub-main
ExecStart=/bin/bash /home/wiktoro/Pobrane/plain-ub-main/run
Restart=always
RestartSec=10
Environment=PATH=/home/wiktoro/bin:/usr/local/bin:/usr/bin:/bin
[Install]
WantedBy=multi-user.target