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:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
config*.env
|
||||
*session*
|
||||
venv/
|
||||
down*
|
||||
__pycache__
|
||||
.idea/
|
||||
conf_backup/
|
||||
logs/
|
||||
.mypy_cache
|
||||
psutil
|
||||
app/temp
|
||||
25
Dockerfile
Normal file
25
Dockerfile
Normal 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
90
README.md
Normal file
@@ -0,0 +1,90 @@
|
||||
## PLAIN UB - OVERSPEND1 FORK
|
||||
|
||||

|
||||

|
||||
|
||||
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
1
app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from ub_core import BOT, LOGGER, Config, Convo, CustomDB, Message, bot
|
||||
13
app/__main__.py
Normal file
13
app/__main__.py
Normal 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
39
app/extra_config.py
Normal 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
29
app/plugins/admin/ban.py
Normal 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
270
app/plugins/admin/fbans.py
Normal 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)
|
||||
77
app/plugins/admin/kicks.py
Normal file
77
app/plugins/admin/kicks.py
Normal 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
35
app/plugins/admin/mute.py
Normal 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)
|
||||
111
app/plugins/admin/promote.py
Normal file
111
app/plugins/admin/promote.py
Normal 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()
|
||||
46
app/plugins/admin/zombies.py
Normal file
46
app/plugins/admin/zombies.py
Normal 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)
|
||||
2
app/plugins/ai/gemini/__init__.py
Normal file
2
app/plugins/ai/gemini/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .client import client, async_client, Response
|
||||
from .config import AIConfig, DB_SETTINGS
|
||||
157
app/plugins/ai/gemini/chat.py
Normal file
157
app/plugins/ai/gemini/chat.py
Normal 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)
|
||||
147
app/plugins/ai/gemini/client.py
Normal file
147
app/plugins/ai/gemini/client.py
Normal 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
|
||||
134
app/plugins/ai/gemini/config.py
Normal file
134
app/plugins/ai/gemini/config.py
Normal 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}
|
||||
90
app/plugins/ai/gemini/query.py
Normal file
90
app/plugins/ai/gemini/query.py
Normal 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,
|
||||
)
|
||||
151
app/plugins/ai/gemini/utils.py
Normal file
151
app/plugins/ai/gemini/utils.py
Normal 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
170
app/plugins/ai/openai.py
Normal 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
|
||||
)
|
||||
)
|
||||
258
app/plugins/files/anonfiles.py
Normal file
258
app/plugins/files/anonfiles.py
Normal 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
288
app/plugins/files/catbox.py
Normal 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
543
app/plugins/files/gdrive.py
Normal 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
300
app/plugins/files/gofile.py
Normal 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
500
app/plugins/files/leech.py
Normal 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)
|
||||
325
app/plugins/files/pixeldrain.py
Normal file
325
app/plugins/files/pixeldrain.py
Normal 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()
|
||||
65
app/plugins/files/rename.py
Normal file
65
app/plugins/files/rename.py
Normal 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()
|
||||
34
app/plugins/files/spoiler.py
Normal file
34
app/plugins/files/spoiler.py
Normal 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)
|
||||
406
app/plugins/files/torrent_leech.py
Normal file
406
app/plugins/files/torrent_leech.py
Normal 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
203
app/plugins/files/upload.py
Normal 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
90
app/plugins/misc/alive.py
Normal 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")],
|
||||
]
|
||||
)
|
||||
13
app/plugins/misc/extra_module_updater.py
Normal file
13
app/plugins/misc/extra_module_updater.py
Normal 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()
|
||||
58
app/plugins/misc/inline_bot_results.py
Normal file
58
app/plugins/misc/inline_bot_results.py
Normal 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
114
app/plugins/misc/song.py
Normal 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 {}
|
||||
420
app/plugins/network/network_tools.py
Normal file
420
app/plugins/network/network_tools.py
Normal 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>")
|
||||
118
app/plugins/sudo/commands.py
Normal file
118
app/plugins/sudo/commands.py
Normal 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,
|
||||
)
|
||||
38
app/plugins/sudo/superuser_toggle.py
Normal file
38
app/plugins/sudo/superuser_toggle.py
Normal 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
187
app/plugins/sudo/users.py
Normal 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)
|
||||
246
app/plugins/system/neofetch.py
Normal file
246
app/plugins/system/neofetch.py
Normal 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
232
app/plugins/system/shell.py
Normal 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)
|
||||
180
app/plugins/system/speedtest.py
Normal file
180
app/plugins/system/speedtest.py
Normal 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>")
|
||||
433
app/plugins/system/sysinfo.py
Normal file
433
app/plugins/system/sysinfo.py
Normal 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>")
|
||||
295
app/plugins/system/termux.py
Normal file
295
app/plugins/system/termux.py
Normal 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>")
|
||||
61
app/plugins/tg_tools/chat.py
Normal file
61
app/plugins/tg_tools/chat.py
Normal 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))
|
||||
14
app/plugins/tg_tools/click.py
Normal file
14
app/plugins/tg_tools/click.py
Normal 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)
|
||||
77
app/plugins/tg_tools/delete.py
Normal file
77
app/plugins/tg_tools/delete.py
Normal 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)
|
||||
50
app/plugins/tg_tools/get_message.py
Normal file
50
app/plugins/tg_tools/get_message.py
Normal 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)}```")
|
||||
228
app/plugins/tg_tools/kang.py
Normal file
228
app/plugins/tg_tools/kang.py
Normal 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)
|
||||
216
app/plugins/tg_tools/legacy_kang.py
Normal file
216
app/plugins/tg_tools/legacy_kang.py
Normal 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)
|
||||
13
app/plugins/tg_tools/ping.py
Normal file
13
app/plugins/tg_tools/ping.py
Normal 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.")
|
||||
223
app/plugins/tg_tools/pm_n_tag_logger.py
Normal file
223
app/plugins/tg_tools/pm_n_tag_logger.py
Normal 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)
|
||||
153
app/plugins/tg_tools/pm_permit.py
Normal file
153
app/plugins/tg_tools/pm_permit.py
Normal 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
|
||||
33
app/plugins/tg_tools/reply.py
Normal file
33
app/plugins/tg_tools/reply.py
Normal 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
|
||||
)
|
||||
27
app/plugins/tg_tools/respond.py
Normal file
27
app/plugins/tg_tools/respond.py
Normal 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
263
app/plugins/utils/calc.py
Normal 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
278
app/plugins/utils/encode.py
Normal 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
186
app/plugins/utils/qr.py
Normal 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
BIN
assets/dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/light.png
Normal file
BIN
assets/light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
39
docker_start.sh
Executable file
39
docker_start.sh
Executable 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
15
install_service.sh
Executable 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
14
req.txt
Normal 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
11
run
Executable 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
88
sample-config.env
Normal 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.
|
||||
14
scripts/install_external_modules.sh
Executable file
14
scripts/install_external_modules.sh
Executable 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
11
scripts/install_termux_reqs.sh
Executable 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
7
scripts/install_ub_core.sh
Executable 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
1
ubot
Submodule
Submodule ubot added at e0a8b22b2e
15
userbot.service
Normal file
15
userbot.service
Normal 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
|
||||
Reference in New Issue
Block a user