From 2fa040829b7e90e064c932a75ed58012df62e69e Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 05:27:00 +0530 Subject: [PATCH 01/16] feat: migrate to hybrid MongoDB/Redis architecture - Migrated SQLAlchemy (Postgres/SQLite) to MongoDB (Motor) and Redis\n- Updated configuration loader to use environment variables\n- Refactored messageRepeater task for MongoDB compatibility\n- Fixed initialization logic and cleaned up legacy SQL files --- .env.example | 3 +- Database/Methods/repeatMethods.py | 183 ++++++++++------------ Database/Tables/base.py | 4 - Database/Tables/loader.py | 7 - Database/Tables/local.py | 9 -- Database/Tables/repeatMessage.py | 17 -- Database/Tables/repeatMessageGroup.py | 14 -- Database/Tables/repeatMessageGroupChat.py | 14 -- Database/client.py | 64 -------- Database/mongo_client.py | 26 +++ Database/redis_client.py | 17 ++ Hazel/Tasks/messageRepeater.py | 45 +++--- Hazel/__init__.py | 7 +- HazelUB.db | Bin 24576 -> 0 bytes MultiSessionManagement/telegram.py | 4 +- Setup/installation.py | 30 ++-- Setup/utils.py | 11 +- config.py | 29 ++-- requirements.txt | 5 +- 19 files changed, 205 insertions(+), 284 deletions(-) delete mode 100644 Database/Tables/base.py delete mode 100644 Database/Tables/loader.py delete mode 100644 Database/Tables/local.py delete mode 100644 Database/Tables/repeatMessage.py delete mode 100644 Database/Tables/repeatMessageGroup.py delete mode 100644 Database/Tables/repeatMessageGroupChat.py delete mode 100644 Database/client.py create mode 100644 Database/mongo_client.py create mode 100644 Database/redis_client.py delete mode 100644 HazelUB.db diff --git a/.env.example b/.env.example index 13134eb..4763a1f 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,8 @@ API_ID= API_HASH= SESSION= BOT_TOKEN= -DB_URL= +MONGO_URL= +REDIS_URL= OtherSessions= PREFIX= GEMINI_API_KEY= \ No newline at end of file diff --git a/Database/Methods/repeatMethods.py b/Database/Methods/repeatMethods.py index 3a84f0c..2ed1dcd 100644 --- a/Database/Methods/repeatMethods.py +++ b/Database/Methods/repeatMethods.py @@ -1,102 +1,92 @@ -from sqlalchemy import select, delete -from Database.Tables.repeatMessage import RepeatMessage -from Database.Tables.repeatMessageGroup import RepeatMessageGroup -from Database.Tables.repeatMessageGroupChat import RepeatMessageGroupChat - - class RepeatMethods: # ---------- Groups ---------- - async def create_group(self, name: str, user_id: int) -> RepeatMessageGroup: - name = (name.replace(' ', '')).lower() # remove spaces and change to lower case - async with self.get_pg() as session: # type: ignore - group = RepeatMessageGroup(userId=user_id, name=name) - session.add(group) - await session.commit() - await session.refresh(group) - return group + async def create_group(self, name: str, user_id: int): + name = (name.replace(' ', '')).lower() + group = { + "userId": user_id, + "name": name + } + result = await self.repeat_groups.insert_one(group) + group["_id"] = result.inserted_id + return group - async def get_group(self, group_id: int, user_id: int): - async with self.get_pg() as session: # type: ignore - x = await session.get(RepeatMessageGroup, group_id) - if x.userId == user_id: # Check if the group belongs to the user. - return x + async def get_group(self, group_id: str, user_id: int): + # Note: Using string IDs for Mongo compatibility or ObjectId if needed + from bson import ObjectId + try: + gid = ObjectId(group_id) if isinstance(group_id, str) else group_id + except: + return None + x = await self.repeat_groups.find_one({"_id": gid}) + if x and x.get("userId") == user_id: + return x async def get_group_by_name(self, name: str, user_id: int): - name = (name.replace(' ', '')).lower() # remove spaces and change to lower case - async with self.get_pg() as session: # type: ignore - q = await session.execute( - select(RepeatMessageGroup).where( - RepeatMessageGroup.name == name - ) - ) - x = q.scalar_one_or_none() - if x and x.userId == user_id: # Check if the group belongs to the user. - return x + name = (name.replace(' ', '')).lower() + x = await self.repeat_groups.find_one({"name": name, "userId": user_id}) + return x async def get_groups(self, user_id: int): - async with self.get_pg() as session: # type: ignore - q = await session.execute( - select(RepeatMessageGroup) - .where(RepeatMessageGroup.userId == user_id) - ) - return q.scalars().all() + cursor = self.repeat_groups.find({"userId": user_id}) + return await cursor.to_list(length=None) - async def delete_group(self, group_id: int, user_id: int): + async def delete_group(self, group_id: str, user_id: int): + from bson import ObjectId + try: + gid = ObjectId(group_id) if isinstance(group_id, str) else group_id + except: + raise Exception('Invalid Group ID') + group = await self.get_group(group_id, user_id) if not group: raise Exception('The group is not found or it does not belongs to you') - async with self.get_pg() as session: # type: ignore - await session.execute( - delete(RepeatMessageGroupChat) - .where(RepeatMessageGroupChat.group_id == group_id) - ) - await session.execute( - delete(RepeatMessageGroup) - .where(RepeatMessageGroup.id == group_id) - ) - await session.commit() + + await self.repeat_group_chats.delete_many({"group_id": gid}) + await self.repeat_groups.delete_one({"_id": gid}) # ---------- Group Chats ---------- - async def add_chat_to_group(self, group_id: int, chat_id: int, user_id: int): + async def add_chat_to_group(self, group_id: str, chat_id: int, user_id: int): + from bson import ObjectId + gid = ObjectId(group_id) if isinstance(group_id, str) else group_id + group = await self.get_group(group_id, user_id) if not group: raise Exception('The group is not found or it does not belongs to you') - async with self.get_pg() as session: # type: ignore - row = RepeatMessageGroupChat( - group_id=group_id, - chat_id=chat_id, - userId=user_id - ) - session.add(row) - await session.commit() - return row + + row = { + "group_id": gid, + "chat_id": chat_id, + "userId": user_id + } + await self.repeat_group_chats.insert_one(row) + return row + + async def remove_chat_from_group(self, group_id: str, chat_id: int, user_id: int): + from bson import ObjectId + gid = ObjectId(group_id) if isinstance(group_id, str) else group_id - async def remove_chat_from_group(self, group_id: int, chat_id: int, user_id: int): group = await self.get_group(group_id, user_id) if not group: raise Exception('The group is not found or it does not belongs to you') - async with self.get_pg() as session: # type: ignore - await session.execute( - delete(RepeatMessageGroupChat) - .where( - RepeatMessageGroupChat.group_id == group_id, - RepeatMessageGroupChat.chat_id == chat_id - ) - ) - await session.commit() + + await self.repeat_group_chats.delete_one({ + "group_id": gid, + "chat_id": chat_id + }) + + async def get_group_chats(self, group_id: str, user_id: int) -> list[int]: + from bson import ObjectId + gid = ObjectId(group_id) if isinstance(group_id, str) else group_id - async def get_group_chats(self, group_id: int, user_id: int) -> list[int]: group = await self.get_group(group_id, user_id) if not group: raise Exception('The group is not found or it does not belongs to you') - async with self.get_pg() as session: # type: ignore - q = await session.execute( - select(RepeatMessageGroupChat.chat_id) - .where(RepeatMessageGroupChat.group_id == group_id) - ) - return [x[0] for x in q.all()] + + cursor = self.repeat_group_chats.find({"group_id": gid}) + chats = await cursor.to_list(length=None) + return [x["chat_id"] for x in chats] # ---------- Repeat Messages ---------- @@ -106,32 +96,27 @@ async def create_repeat_message( userId: int, message_id: int, source_chat_id: int, - group_id: int + group_id: str ): - async with self.get_pg() as session: # type: ignore - row = RepeatMessage( - repeatTime=repeatTime, - userId=userId, - message_id=message_id, - source_chat_id=source_chat_id, - group_id=group_id - ) - session.add(row) - await session.commit() - await session.refresh(row) - return row + from bson import ObjectId + gid = ObjectId(group_id) if isinstance(group_id, str) else group_id + + row = { + "repeatTime": repeatTime, + "userId": userId, + "message_id": message_id, + "source_chat_id": source_chat_id, + "group_id": gid + } + result = await self.repeat_messages.insert_one(row) + row["_id"] = result.inserted_id + return row - async def get_repeat_messages(self) -> list[RepeatMessage]: - async with self.get_pg() as session: # type: ignore - q = await session.execute( - select(RepeatMessage) - ) - return q.scalars().all() + async def get_repeat_messages(self): + cursor = self.repeat_messages.find({}) + return await cursor.to_list(length=None) - async def delete_repeat_message(self, repeat_id: int): - async with self.get_pg() as session: # type: ignore - await session.execute( - delete(RepeatMessage) - .where(RepeatMessage.id == repeat_id) - ) - await session.commit() \ No newline at end of file + async def delete_repeat_message(self, repeat_id: str): + from bson import ObjectId + rid = ObjectId(repeat_id) if isinstance(repeat_id, str) else repeat_id + await self.repeat_messages.delete_one({"_id": rid}) \ No newline at end of file diff --git a/Database/Tables/base.py b/Database/Tables/base.py deleted file mode 100644 index f75ec44..0000000 --- a/Database/Tables/base.py +++ /dev/null @@ -1,4 +0,0 @@ -from sqlalchemy.orm import DeclarativeBase - -class Base(DeclarativeBase): - pass diff --git a/Database/Tables/loader.py b/Database/Tables/loader.py deleted file mode 100644 index b944785..0000000 --- a/Database/Tables/loader.py +++ /dev/null @@ -1,7 +0,0 @@ -import pkgutil -import importlib -import Database.Tables - -def load_models(): - for _, module_name, _ in pkgutil.iter_modules(Database.Tables.__path__): - importlib.import_module(f"Database.Tables.{module_name}") diff --git a/Database/Tables/local.py b/Database/Tables/local.py deleted file mode 100644 index 263e26a..0000000 --- a/Database/Tables/local.py +++ /dev/null @@ -1,9 +0,0 @@ -from sqlalchemy import Boolean, Integer -from sqlalchemy.orm import Mapped, mapped_column -from .base import Base - -class LocalState(Base): - __tablename__ = "local_state" - - id: Mapped[int] = mapped_column(Integer, primary_key=True) - installed: Mapped[bool] = mapped_column(Boolean, default=False) diff --git a/Database/Tables/repeatMessage.py b/Database/Tables/repeatMessage.py deleted file mode 100644 index 7cb1234..0000000 --- a/Database/Tables/repeatMessage.py +++ /dev/null @@ -1,17 +0,0 @@ -from sqlalchemy import Column, Integer, BigInteger, ForeignKey -from .base import Base - -class RepeatMessage(Base): - __tablename__ = "repeat_messages" - - id = Column(Integer, primary_key=True) - repeatTime = Column(Integer, default=1, nullable=False) - userId = Column(BigInteger, nullable=False) - message_id = Column(BigInteger, nullable=False) - source_chat_id = Column(BigInteger, nullable=False) - - group_id = Column( - Integer, - ForeignKey("repeat_message_groups.id"), - nullable=False - ) diff --git a/Database/Tables/repeatMessageGroup.py b/Database/Tables/repeatMessageGroup.py deleted file mode 100644 index 1555851..0000000 --- a/Database/Tables/repeatMessageGroup.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlalchemy import UniqueConstraint -from sqlalchemy import Column, Integer, String, BigInteger -from .base import Base - -class RepeatMessageGroup(Base): - __tablename__ = "repeat_message_groups" - - id = Column(Integer, primary_key=True) - userId = Column(BigInteger, nullable=False) - name = Column(String(50), nullable=False) - - __table_args__ = ( - UniqueConstraint("userId", "name", name="uq_user_group_name"), - ) diff --git a/Database/Tables/repeatMessageGroupChat.py b/Database/Tables/repeatMessageGroupChat.py deleted file mode 100644 index 32e579b..0000000 --- a/Database/Tables/repeatMessageGroupChat.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlalchemy import Column, Integer, BigInteger, ForeignKey, UniqueConstraint -from .base import Base - -class RepeatMessageGroupChat(Base): - __tablename__ = "repeat_message_group_chats" - - id = Column(Integer, primary_key=True) - userId = Column(BigInteger, nullable=False) - group_id = Column(Integer, ForeignKey("repeat_message_groups.id"), nullable=False) - chat_id = Column(BigInteger, nullable=False) - - __table_args__ = ( - UniqueConstraint("group_id", "chat_id", name="uq_group_chat"), - ) diff --git a/Database/client.py b/Database/client.py deleted file mode 100644 index b70bdbe..0000000 --- a/Database/client.py +++ /dev/null @@ -1,64 +0,0 @@ -from sqlalchemy.ext.asyncio import ( - create_async_engine, - async_sessionmaker, - AsyncSession, -) -from .Tables.base import Base -from .Tables.loader import load_models -from .Tables.local import LocalState -from .Methods import Methods - -class DBClient(Methods): - def __init__(self, pg_url: str, sqlite_path="HazelUB.db"): - self.pg_url = pg_url - self.sqlite_url = f"sqlite+aiosqlite:///{sqlite_path}" - - load_models() - - # Engines - self.pg_engine = create_async_engine(pg_url, echo=False) - self.local_engine = create_async_engine(self.sqlite_url, echo=False) - - # Sessions - self.pg_session = async_sessionmaker( - self.pg_engine, expire_on_commit=False - ) - self.local_session = async_sessionmaker( - self.local_engine, expire_on_commit=False - ) - - async def init(self): - # Create tables on both DBs - async with self.pg_engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - - async with self.local_engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - - # Ensure local row exists - async with self.local_session() as session: - res = await session.get(LocalState, 1) - if not res: - session.add(LocalState(id=1, installed=False)) - await session.commit() - - # ---------- Local helpers ---------- - - async def is_installed(self) -> bool: - async with self.local_session() as session: - row = await session.get(LocalState, 1) - return row.installed # type: ignore - - async def set_installed(self, value: bool): - async with self.local_session() as session: - row = await session.get(LocalState, 1) - row.installed = value # type: ignore - await session.commit() - - # ---------- Generic access ---------- - - def get_pg(self) -> AsyncSession: - return self.pg_session() - - def get_local(self) -> AsyncSession: - return self.local_session() diff --git a/Database/mongo_client.py b/Database/mongo_client.py new file mode 100644 index 0000000..8cc9903 --- /dev/null +++ b/Database/mongo_client.py @@ -0,0 +1,26 @@ +from motor.motor_asyncio import AsyncIOMotorClient +from .Methods.repeatMethods import RepeatMethods + +class MongoClient(RepeatMethods): + def __init__(self, mongo_url: str): + self._client = AsyncIOMotorClient(mongo_url) + self.db = self._client["UBdrag"] + + # Collections + self.repeat_messages = self.db["repeat_messages"] + self.repeat_groups = self.db["repeat_groups"] + self.repeat_group_chats = self.db["repeat_group_chats"] + self.local_state = self.db["local_state"] + + async def init(self): + # Ensure local row exists in Mongo + res = await self.local_state.find_one({"id": 1}) + if not res: + await self.local_state.insert_one({"id": 1, "installed": False}) + + async def is_installed(self) -> bool: + row = await self.local_state.find_one({"id": 1}) + return row.get("installed", False) if row else False + + async def set_installed(self, value: bool): + await self.local_state.update_one({"id": 1}, {"$set": {"installed": value}}) diff --git a/Database/redis_client.py b/Database/redis_client.py new file mode 100644 index 0000000..ea5f621 --- /dev/null +++ b/Database/redis_client.py @@ -0,0 +1,17 @@ +import redis.asyncio as redis + +class RedisClient: + def __init__(self, redis_url: str): + self.redis = redis.from_url(redis_url, decode_responses=True) + + async def set(self, key: str, value: str, ex=None): + await self.redis.set(key, value, ex=ex) + + async def get(self, key: str): + return await self.redis.get(key) + + async def delete(self, key: str): + await self.redis.delete(key) + + async def exists(self, key: str): + return await self.redis.exists(key) diff --git a/Hazel/Tasks/messageRepeater.py b/Hazel/Tasks/messageRepeater.py index 8cef1ee..6929645 100644 --- a/Hazel/Tasks/messageRepeater.py +++ b/Hazel/Tasks/messageRepeater.py @@ -2,52 +2,59 @@ import logging from MultiSessionManagement.telegram import Telegram from pyrogram.client import Client -from Database.client import DBClient -from Database.Tables.repeatMessage import RepeatMessage from pyrogram.errors import FloodWait -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING, Dict, Any import traceback logger = logging.getLogger("Hazel.Tasks.messageRepeater") if TYPE_CHECKING: + from Database.mongo_client import MongoClient events: Dict[int, asyncio.Event] # int is client's user_id else: + MongoClient = Any events = {} -async def createJob(job: RepeatMessage, chats: list, client: Client): +async def createJob(job: dict, chats: list, client: Client): while True: try: - await asyncio.sleep(int(job.repeatTime * 60)) # type: ignore # Convert sec to mins - if job.userId in events: - await events[job.userId].wait() # type: ignore - chat_id: int = 0 + repeat_time = job.get("repeatTime", 1) + await asyncio.sleep(int(repeat_time * 60)) # type: ignore # Convert sec to mins + + user_id = job.get("userId") + if user_id in events: + await events[user_id].wait() # type: ignore + for chat_id in chats: try: await client.copy_message( chat_id=chat_id, - from_chat_id=job.source_chat_id, # type: ignore - message_id=job.message_id # type: ignore + from_chat_id=job.get("source_chat_id"), # type: ignore + message_id=job.get("message_id") # type: ignore ) await asyncio.sleep(2.59) except FloodWait as e: logger.critical(f"FloodWaitError: Sleeping for {e.value}.") await asyncio.sleep(e.value) # type: ignore except Exception as e: - logger.error(f"Could not send repeat message for user: {client.me.id} at chat: {chat_id}. RepeatMessage ID: {job.id}. Error: {e}") # type: ignore + logger.error(f"Could not send repeat message for user: {client.me.id} at chat: {chat_id}. RepeatMessage ID: {job.get('_id')}. Error: {e}") # type: ignore except Exception as e: - logger.error(f"Failed to do repeat task for user: {job.userId}: Full Traceback: {traceback.format_exc()}") + logger.error(f"Failed to do repeat task for user: {job.get('userId')}: Full Traceback: {traceback.format_exc()}") -async def main(Tele: Telegram, db: DBClient): +async def main(Tele: Telegram, db: MongoClient): jobs = await db.get_repeat_messages() for job in jobs: for client in Tele._allClients: - if client.me.id == job.userId: # type: ignore - if job.userId not in events: - events[job.userId] = asyncio.Event() # type: ignore - events[job.userId].set() # type: ignore - chats = await db.get_group_chats(job.group_id, user_id=client.me.id) # type: ignore + # client.me might be None if not started, but Tele.start (in Setup/main.py) is called before this. + if client.me and client.me.id == job.get("userId"): # type: ignore + user_id = job.get("userId") + if user_id not in events: + events[user_id] = asyncio.Event() # type: ignore + events[user_id].set() # type: ignore + + group_id = job.get("group_id") + chats = await db.get_group_chats(group_id, user_id=client.me.id) # type: ignore asyncio.create_task(createJob(job, chats, client)) - logger.info(f"Created repeat message job for user {job.userId} in group {job.group_id}") # type: ignore + logger.info(f"Created repeat message job for user {user_id} in group {group_id}") # type: ignore logger.info(f"Loaded {len(jobs)} repeat message jobs.") \ No newline at end of file diff --git a/Hazel/__init__.py b/Hazel/__init__.py index 8ae263d..79905f9 100644 --- a/Hazel/__init__.py +++ b/Hazel/__init__.py @@ -4,12 +4,15 @@ if TYPE_CHECKING: from MultiSessionManagement.telegram import Telegram - import Database.client as Database + from Database.mongo_client import MongoClient + from Database.redis_client import RedisClient Tele: Telegram - SQLClient: Database.DBClient + SQLClient: MongoClient + Redis: RedisClient else: Tele = None SQLClient = None + Redis = None logging.Formatter.converter = lambda *args: time.gmtime(time.time() + 19800) diff --git a/HazelUB.db b/HazelUB.db deleted file mode 100644 index 291757828644afe524f6a46d2b9f5ff801a561fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI%(TmeC90%~^dfRTrIR#;_VJFfu5k+LL_C{_Nd!6ex;U1*aEuMvLJ=acs-RYD6 zvM;9H74MF$Pd-TbhJ|KHn)LTc`Vcx9J7Fr=r+B>#QudNOAe55Vj1fYPWUk9xq(fQA za*=3JD7hYyXTKh`+A*o!W2F7<{@3>7))QGof&c^{009U<00Izz00jO+fh(=n=vWs0 z@h}ZOMq({4MUeVSktD%|@GsW!=F*>k4$@?QOMT#RYs%TwI&nDL-;Wu3Gh8s+o$?{~ zm^+#>cjh=xncm#Z^S3dp%Vzau)rmcnx7_Of&a=-f@0`8m=gbHfX4d_5;ect00Izz00bZa n0SG_<0uX>e^#ySLul^b%LI^+r0uX=z1Rwwb2tWV=5P-lRTqZvk diff --git a/MultiSessionManagement/telegram.py b/MultiSessionManagement/telegram.py index 3b18a56..27ea80b 100644 --- a/MultiSessionManagement/telegram.py +++ b/MultiSessionManagement/telegram.py @@ -13,7 +13,7 @@ class Telegram(Methods, Decorators): def __init__(self, config: tuple) -> None: # ----------- Config--------- self.session: str = config[3] - self.othersessions: List[str] = config[5] + self.othersessions: List[str] = config[6] self.api_id: int = int(config[1]) self.api_hash: str = config[2] self.bot_token: str = config[0] @@ -27,7 +27,7 @@ def __init__(self, config: tuple) -> None: self._clientPrivileges: Dict[Client, str] = {} self._clientPyTgCalls: Dict[Client, PyTgCalls] = {} - filters.command = partial(filters.command, prefixes=config[6]) # Override filters.command to set defualt prefixes + filters.command = partial(filters.command, prefixes=config[7]) # Override filters.command to set defualt prefixes async def create_pyrogram_clients(self) -> None: if len(self.bot_token) > 50: # Bot Client diff --git a/Setup/installation.py b/Setup/installation.py index 4c3dc28..e169600 100644 --- a/Setup/installation.py +++ b/Setup/installation.py @@ -1,17 +1,21 @@ import Hazel +import os from logging import getLogger from restart import restart from typing import TYPE_CHECKING, Tuple from .utils import install_requirements, load_config, clear +from dotenv import load_dotenv if TYPE_CHECKING: - from Database.client import DBClient + from Database.mongo_client import MongoClient + from Database.redis_client import RedisClient else: - DBClient = None + MongoClient = None + RedisClient = None logger = getLogger(__name__) -async def main() -> Tuple[DBClient, tuple]: +async def main() -> Tuple[MongoClient, tuple]: clear() print( "HazelUB is now booting...\n" @@ -20,10 +24,8 @@ async def main() -> Tuple[DBClient, tuple]: ) try: # Checking once if essential packages are installed. - from dotenv import load_dotenv - from sqlalchemy import create_engine - from asyncpg import create_pool - from aiosqlite import connect + from motor.motor_asyncio import AsyncIOMotorClient + from redis import asyncio as redis from art import text2art from pyrogram.client import Client from google import genai @@ -36,13 +38,21 @@ async def main() -> Tuple[DBClient, tuple]: else: raise SystemExit(f"Setup Failed: Could not install required packages: {install_status}") - from Database.client import DBClient + from Database.mongo_client import MongoClient + from Database.redis_client import RedisClient load_dotenv() config = load_config() - db = DBClient(config[4]) + + # Init Mongo + db = MongoClient(config[4]) await db.init() - Hazel.SQLClient = db # Override SQLClient in Hazel.__init__ + Hazel.SQLClient = db + + # Init Redis + redis_url = config[5] + if redis_url: + Hazel.Redis = RedisClient(redis_url) is_installed = await db.is_installed() if not is_installed: diff --git a/Setup/utils.py b/Setup/utils.py index 2e63694..e82d41a 100644 --- a/Setup/utils.py +++ b/Setup/utils.py @@ -45,12 +45,13 @@ def load_config() -> tuple: API_ID = config.API_ID or os.getenv('API_ID') or _ask_missing("API_ID") API_HASH = config.API_HASH or os.getenv('API_HASH') or _ask_missing("API_HASH") SESSION = config.SESSION or os.getenv('SESSION') or _ask_missing("SESSION") - DB_URL = config.DB_URL or os.getenv('DB_URL') or _ask_missing("DB_URL") + MONGO_URL = getattr(config, 'MONGO_URL', None) or os.getenv('MONGO_URL') or _ask_missing("MONGO_URL") + REDIS_URL = getattr(config, 'REDIS_URL', None) or os.getenv('REDIS_URL') # ---------- Optional ---------- - OtherSessions = config.OtherSessions or list(os.getenv('OtherSessions', [])) - PREFIX = list(config.PREFIX) or os.getenv('PREFIX', []) - GEMINI_API_KEY = config.GEMINI_API_KEY or os.getenv('GEMINI_API_KEY', '') - return (BOT_TOKEN, API_ID, API_HASH, SESSION, DB_URL, OtherSessions, PREFIX, GEMINI_API_KEY) + OtherSessions = getattr(config, 'OtherSessions', []) or list(os.getenv('OtherSessions', [])) + PREFIX = list(getattr(config, 'PREFIX', [])) or os.getenv('PREFIX', []) + GEMINI_API_KEY = getattr(config, 'GEMINI_API_KEY', '') or os.getenv('GEMINI_API_KEY', '') + return (BOT_TOKEN, API_ID, API_HASH, SESSION, MONGO_URL, REDIS_URL, OtherSessions, PREFIX, GEMINI_API_KEY) def startup_popup(): from plyer import notification diff --git a/config.py b/config.py index 49a2da0..b8e799b 100644 --- a/config.py +++ b/config.py @@ -1,16 +1,17 @@ -API_ID = "10187126" -API_HASH = "ff197c0d23d7fe54c89b44ed092c1752" -SESSION = "" # Example: "sessionqwertyuiopasdfghjkl..." -BOT_TOKEN = "" -DB_URL = "" # PostgreSQL URL -OtherSessions = [] # Example: ["session1", "session2", ...] -PREFIX = [".","~","$","^"] -GEMINI_API_KEY = "" # Get it from https://aistudio.google.com/app/api-keys +import os +from dotenv import load_dotenv -# No need to change (almost all time) -DazzerBot = "@Dazzerbot" # This bot allows us to download songs +load_dotenv() + +API_ID = os.getenv("API_ID", "10187126") +API_HASH = os.getenv("API_HASH", "ff197c0d23d7fe54c89b44ed092c1752") +SESSION = os.getenv("SESSION", "") +BOT_TOKEN = os.getenv("BOT_TOKEN", "") +MONGO_URL = os.getenv("MONGO_URL", "") +REDIS_URL = os.getenv("REDIS_URL", "") +OtherSessions = os.getenv("OtherSessions", "").split() +PREFIX = os.getenv("PREFIX", ". ~ $ ^").split() +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") -""" -* Pyrogram session(s) are only allowed. -* You can use Bot's session in BOT_TOKEN var. to avoid login errors. -""" \ No newline at end of file +# No need to change (almost all time) +DazzerBot = "@Dazzerbot" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0265de4..9f8a324 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,8 +8,7 @@ google numpy python-dotenv plyer -SQLAlchemy[asyncio] -asyncpg -aiosqlite +motor +redis websockets google-genai \ No newline at end of file From 31fd694fba6676fa5e9c35fb809c35bb144832f3 Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 05:58:46 +0530 Subject: [PATCH 02/16] feat: add ping, uptime, and pong commands with inline support --- Hazel/__init__.py | 2 +- Mods/ping.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 Mods/ping.py diff --git a/Hazel/__init__.py b/Hazel/__init__.py index 79905f9..d159e83 100644 --- a/Hazel/__init__.py +++ b/Hazel/__init__.py @@ -1,5 +1,5 @@ -import logging import time +START_TIME = time.time() from typing import TYPE_CHECKING, List if TYPE_CHECKING: diff --git a/Mods/ping.py b/Mods/ping.py new file mode 100644 index 0000000..2d3b5c2 --- /dev/null +++ b/Mods/ping.py @@ -0,0 +1,130 @@ +from Hazel import Tele, START_TIME, __version__ +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.types import ( + Message, + InlineKeyboardMarkup, + InlineKeyboardButton, + InlineQuery, + InlineQueryResultArticle, + InputTextMessageContent +) +import time +from datetime import datetime + +def get_readable_time(seconds: int) -> str: + count = 0 + ping_time = "" + time_list = [] + time_suffix_list = ["s", "m", "h", "days"] + + while count < 4: + count += 1 + if count < 3: + remainder, result = divmod(seconds, 60) + else: + remainder, result = divmod(seconds, 24) + if seconds == 0 and remainder == 0: + break + time_list.append(int(result)) + seconds = int(remainder) + + for x in range(len(time_list)): + time_list[x] = str(time_list[x]) + time_suffix_list[x] + if len(time_list) == 4: + ping_time += time_list.pop() + ", " + + time_list.reverse() + ping_time += ":".join(time_list) + + return ping_time + +@Tele.on_message(filters.command("ping") & filters.me) +async def ping_cmd(c: Client, m: Message): + bot_username = (await Tele.bot.get_me()).username + results = await c.get_inline_bot_results(bot_username, "ping") + if results.results: + await c.send_inline_bot_result( + m.chat.id, + results.query_id, + results.results[0].id + ) + await m.delete() + +@Tele.on_message(filters.command("uptime") & filters.me) +async def uptime_cmd(c: Client, m: Message): + bot_username = (await Tele.bot.get_me()).username + results = await c.get_inline_bot_results(bot_username, "uptime") + if results.results: + await c.send_inline_bot_result( + m.chat.id, + results.query_id, + results.results[0].id + ) + await m.delete() + +@Tele.on_message(filters.command("pong") & filters.me) +async def pong_cmd(c: Client, m: Message): + start = datetime.now() + end = datetime.now() + ms = (end - start).microseconds / 1000 + await m.edit(f"**Pong!**\n`{ms}ms`") + +# --- Inline Handlers --- + +@Tele.bot.on_inline_query(filters.regex("^ping$")) +async def ping_inline(c: Client, q: InlineQuery): + start = time.time() + # We can't easily measure round trip here without a message, + # but we can show bot's internal latency/uptime. + uptime = get_readable_time(int(time.time() - START_TIME)) + + await q.answer([ + InlineQueryResultArticle( + title="Ping", + description="Check bot latency and uptime.", + input_message_content=InputTextMessageContent( + f"**HazelUB Stats**\n" + f"![⚡](tg://emoji?id=5328303038660613219) **Latency:** `calculating...`\n" + f"![🕒](tg://emoji?id=5328303038660613219) **Uptime:** `{uptime}`\n" + f"![🤖](tg://emoji?id=5328303038660613219) **Version:** `{__version__}`" + ), + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("Refresh", callback_data="ping_refresh")] + ]) + ) + ], cache_time=0) + +@Tele.bot.on_inline_query(filters.regex("^uptime$")) +async def uptime_inline(c: Client, q: InlineQuery): + uptime = get_readable_time(int(time.time() - START_TIME)) + await q.answer([ + InlineQueryResultArticle( + title="Uptime", + description=f"Bot has been running for {uptime}", + input_message_content=InputTextMessageContent( + f"![🚀](tg://emoji?id=5328303038660613219) **HazelUB Uptime:** `{uptime}`" + ) + ) + ], cache_time=0) + +@Tele.bot.on_callback_query(filters.regex("ping_refresh")) +async def ping_refresh_cb(c: Client, q): + start = time.time() + await q.answer("Refreshing...") + end = time.time() + ms = round((end - start) * 1000, 2) + uptime = get_readable_time(int(time.time() - START_TIME)) + + await q.edit_message_text( + f"**HazelUB Stats**\n" + f"![⚡](tg://emoji?id=5328303038660613219) **Latency:** `{ms}ms`\n" + f"![🕒](tg://emoji?id=5328303038660613219) **Uptime:** `{uptime}`\n" + f"![🤖](tg://emoji?id=5328303038660613219) **Version:** `{__version__}`", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("Refresh", callback_data="ping_refresh")] + ]) + ) + +MOD_NAME = "Ping" +MOD_HELP = "Check bot latency and uptime.\n\nUsage:\n> .ping - Stats with latency.\n> .uptime - Show uptime.\n> .pong - Simple reply." From 60175f3974dc0da5b8453a3adf93fbc59a154cec Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 05:59:03 +0530 Subject: [PATCH 03/16] fix: restore missing logging import in Hazel core --- Hazel/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Hazel/__init__.py b/Hazel/__init__.py index d159e83..2c1ac7c 100644 --- a/Hazel/__init__.py +++ b/Hazel/__init__.py @@ -1,3 +1,4 @@ +import logging import time START_TIME = time.time() from typing import TYPE_CHECKING, List From a6527629480a152b6999ff797758a8ea3e9880cf Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 06:07:42 +0530 Subject: [PATCH 04/16] refactor: simplify ping command output --- Mods/ping.py | 65 +++++++--------------------------------------------- 1 file changed, 8 insertions(+), 57 deletions(-) diff --git a/Mods/ping.py b/Mods/ping.py index 2d3b5c2..2d413a2 100644 --- a/Mods/ping.py +++ b/Mods/ping.py @@ -1,4 +1,4 @@ -from Hazel import Tele, START_TIME, __version__ +from Hazel import Tele, START_TIME from pyrogram import filters from pyrogram.client import Client from pyrogram.types import ( @@ -53,78 +53,29 @@ async def ping_cmd(c: Client, m: Message): @Tele.on_message(filters.command("uptime") & filters.me) async def uptime_cmd(c: Client, m: Message): - bot_username = (await Tele.bot.get_me()).username - results = await c.get_inline_bot_results(bot_username, "uptime") - if results.results: - await c.send_inline_bot_result( - m.chat.id, - results.query_id, - results.results[0].id - ) - await m.delete() + uptime = get_readable_time(int(time.time() - START_TIME)) + await m.edit(f"🚀 **Uptime:** `{uptime}`") @Tele.on_message(filters.command("pong") & filters.me) async def pong_cmd(c: Client, m: Message): - start = datetime.now() - end = datetime.now() - ms = (end - start).microseconds / 1000 - await m.edit(f"**Pong!**\n`{ms}ms`") + await m.edit("**Pong!**") # --- Inline Handlers --- @Tele.bot.on_inline_query(filters.regex("^ping$")) async def ping_inline(c: Client, q: InlineQuery): - start = time.time() - # We can't easily measure round trip here without a message, - # but we can show bot's internal latency/uptime. uptime = get_readable_time(int(time.time() - START_TIME)) await q.answer([ InlineQueryResultArticle( title="Ping", - description="Check bot latency and uptime.", + description="Check bot status.", input_message_content=InputTextMessageContent( - f"**HazelUB Stats**\n" - f"![⚡](tg://emoji?id=5328303038660613219) **Latency:** `calculating...`\n" - f"![🕒](tg://emoji?id=5328303038660613219) **Uptime:** `{uptime}`\n" - f"![🤖](tg://emoji?id=5328303038660613219) **Version:** `{__version__}`" - ), - reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton("Refresh", callback_data="ping_refresh")] - ]) - ) - ], cache_time=0) - -@Tele.bot.on_inline_query(filters.regex("^uptime$")) -async def uptime_inline(c: Client, q: InlineQuery): - uptime = get_readable_time(int(time.time() - START_TIME)) - await q.answer([ - InlineQueryResultArticle( - title="Uptime", - description=f"Bot has been running for {uptime}", - input_message_content=InputTextMessageContent( - f"![🚀](tg://emoji?id=5328303038660613219) **HazelUB Uptime:** `{uptime}`" + f"**Pong!**\n" + f"🕒 **Uptime:** `{uptime}`" ) ) ], cache_time=0) -@Tele.bot.on_callback_query(filters.regex("ping_refresh")) -async def ping_refresh_cb(c: Client, q): - start = time.time() - await q.answer("Refreshing...") - end = time.time() - ms = round((end - start) * 1000, 2) - uptime = get_readable_time(int(time.time() - START_TIME)) - - await q.edit_message_text( - f"**HazelUB Stats**\n" - f"![⚡](tg://emoji?id=5328303038660613219) **Latency:** `{ms}ms`\n" - f"![🕒](tg://emoji?id=5328303038660613219) **Uptime:** `{uptime}`\n" - f"![🤖](tg://emoji?id=5328303038660613219) **Version:** `{__version__}`", - reply_markup=InlineKeyboardMarkup([ - [InlineKeyboardButton("Refresh", callback_data="ping_refresh")] - ]) - ) - MOD_NAME = "Ping" -MOD_HELP = "Check bot latency and uptime.\n\nUsage:\n> .ping - Stats with latency.\n> .uptime - Show uptime.\n> .pong - Simple reply." +MOD_HELP = "Check bot status.\n\nUsage:\n> .ping - Show Pong and Uptime via inline.\n> .uptime - Show uptime directly.\n> .pong - Simple reply." From d762447d4345de06863792767a511f69ed990bef Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 06:09:52 +0530 Subject: [PATCH 05/16] style: match ping output to user preference --- Mods/ping.py | 57 ++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/Mods/ping.py b/Mods/ping.py index 2d413a2..1c54253 100644 --- a/Mods/ping.py +++ b/Mods/ping.py @@ -14,30 +14,29 @@ def get_readable_time(seconds: int) -> str: count = 0 - ping_time = "" time_list = [] - time_suffix_list = ["s", "m", "h", "days"] + time_suffix_list = ["s", "m", "h", "d", "w"] # Adding week support for the requested format - while count < 4: - count += 1 - if count < 3: - remainder, result = divmod(seconds, 60) - else: - remainder, result = divmod(seconds, 24) - if seconds == 0 and remainder == 0: - break - time_list.append(int(result)) - seconds = int(remainder) + # Years + # ... ignoring years for now as boot time is unlikely to be that long - for x in range(len(time_list)): - time_list[x] = str(time_list[x]) + time_suffix_list[x] - if len(time_list) == 4: - ping_time += time_list.pop() + ", " - - time_list.reverse() - ping_time += ":".join(time_list) - - return ping_time + # Weeks + weeks, seconds = divmod(seconds, 604800) + # Days + days, seconds = divmod(seconds, 86400) + # Hours + hours, seconds = divmod(seconds, 3600) + # Minutes + minutes, seconds = divmod(seconds, 60) + + parts = [] + if weeks: parts.append(f"{int(weeks)}w") + if days: parts.append(f"{int(days)}d") + if hours: parts.append(f"{int(hours)}h") + if minutes: parts.append(f"{int(minutes)}m") + parts.append(f"{int(seconds)}s") + + return ":".join(parts) @Tele.on_message(filters.command("ping") & filters.me) async def ping_cmd(c: Client, m: Message): @@ -54,28 +53,28 @@ async def ping_cmd(c: Client, m: Message): @Tele.on_message(filters.command("uptime") & filters.me) async def uptime_cmd(c: Client, m: Message): uptime = get_readable_time(int(time.time() - START_TIME)) - await m.edit(f"🚀 **Uptime:** `{uptime}`") - -@Tele.on_message(filters.command("pong") & filters.me) -async def pong_cmd(c: Client, m: Message): - await m.edit("**Pong!**") + await m.edit(f"**Uptime -** `{uptime}`") # --- Inline Handlers --- @Tele.bot.on_inline_query(filters.regex("^ping$")) async def ping_inline(c: Client, q: InlineQuery): + start = time.time() + # We use a dummy wait or just calculate current latency to show something uptime = get_readable_time(int(time.time() - START_TIME)) + end = time.time() + ms = round((end - start) * 1000, 2) await q.answer([ InlineQueryResultArticle( title="Ping", description="Check bot status.", input_message_content=InputTextMessageContent( - f"**Pong!**\n" - f"🕒 **Uptime:** `{uptime}`" + f"**Pong !!** `{ms}ms`\n" + f"**Uptime -** `{uptime}`" ) ) ], cache_time=0) MOD_NAME = "Ping" -MOD_HELP = "Check bot status.\n\nUsage:\n> .ping - Show Pong and Uptime via inline.\n> .uptime - Show uptime directly.\n> .pong - Simple reply." +MOD_HELP = "Check bot status.\n\nUsage:\n> .ping - Show Pong and Uptime in the requested format." From 6e8f0f677b4b5c8abffe9c664861cf8084d84650 Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 13:20:26 +0530 Subject: [PATCH 06/16] feat: implement dynamic multi-client management via bot /login --- Database/Methods/__init__.py | 4 +- Database/Methods/sessionMethods.py | 21 ++++++++ Database/mongo_client.py | 3 +- Mods/ai.py | 57 ++++++++++++++------- Mods/bot_management.py | 67 ++++++++++++++++++++++++ Mods/repeater.py | 67 ++++++++++++------------ MultiSessionManagement/telegram.py | 81 ++++++++++++++++++++++++++---- 7 files changed, 235 insertions(+), 65 deletions(-) create mode 100644 Database/Methods/sessionMethods.py create mode 100644 Mods/bot_management.py diff --git a/Database/Methods/__init__.py b/Database/Methods/__init__.py index 0558a17..f9fe9a1 100644 --- a/Database/Methods/__init__.py +++ b/Database/Methods/__init__.py @@ -1,6 +1,8 @@ from .repeatMethods import RepeatMethods +from .sessionMethods import SessionMethods class Methods( - RepeatMethods + RepeatMethods, + SessionMethods ): pass \ No newline at end of file diff --git a/Database/Methods/sessionMethods.py b/Database/Methods/sessionMethods.py new file mode 100644 index 0000000..ef8a41d --- /dev/null +++ b/Database/Methods/sessionMethods.py @@ -0,0 +1,21 @@ +from typing import List, Optional + +class SessionMethods: + async def add_session(self, session_string: str) -> bool: + """Add a new session string to the database if it doesn't already exist.""" + existing = await self.db["sessions"].find_one({"session": session_string}) + if not existing: + await self.db["sessions"].insert_one({"session": session_string}) + return True + return False + + async def get_all_sessions(self) -> List[str]: + """Retrieve all extra session strings from the database.""" + cursor = self.db["sessions"].find({}) + sessions = await cursor.to_list(length=100) + return [s["session"] for s in sessions] + + async def remove_session(self, session_string: str) -> bool: + """Remove a session string from the database.""" + result = await self.db["sessions"].delete_one({"session": session_string}) + return result.deleted_count > 0 diff --git a/Database/mongo_client.py b/Database/mongo_client.py index 8cc9903..0ae80a5 100644 --- a/Database/mongo_client.py +++ b/Database/mongo_client.py @@ -1,7 +1,8 @@ from motor.motor_asyncio import AsyncIOMotorClient from .Methods.repeatMethods import RepeatMethods +from .Methods.sessionMethods import SessionMethods -class MongoClient(RepeatMethods): +class MongoClient(RepeatMethods, SessionMethods): def __init__(self, mongo_url: str): self._client = AsyncIOMotorClient(mongo_url) self.db = self._client["UBdrag"] diff --git a/Mods/ai.py b/Mods/ai.py index 56ee42a..158e585 100644 --- a/Mods/ai.py +++ b/Mods/ai.py @@ -7,31 +7,44 @@ import logging import os import asyncio -from typing import Dict +from typing import Dict, Optional from datetime import datetime from zoneinfo import ZoneInfo +import config logger = logging.getLogger("Mods.ai") # Load API key -import config API_KEY = config.GEMINI_API_KEY or os.getenv("GEMINI_API_KEY") -if not API_KEY: - logger.critical("GEMINI_API_KEY not found in config or environment. Ai features will not work.") +# Global client, initialized only if API_KEY exists +GENAI_CLIENT: Optional[genai.Client] = None -# Create client -GENAI_CLIENT = genai.Client(api_key=API_KEY) +if not API_KEY: + logger.warning("GEMINI_API_KEY not found. AI features will be disabled.") +else: + try: + GENAI_CLIENT = genai.Client(api_key=API_KEY) + except Exception as e: + logger.error(f"Failed to initialize Gemini Client: {e}") + GENAI_CLIENT = None # Store chat sessions per user AI_SESSIONS: Dict[int, Chat] = {} -def get_ai_session(user_id: int) -> Chat: +def get_ai_session(user_id: int) -> Optional[Chat]: + if not GENAI_CLIENT: + return None + if user_id not in AI_SESSIONS: - AI_SESSIONS[user_id] = GENAI_CLIENT.chats.create( - model="models/gemini-2.5-flash" - ) + try: + AI_SESSIONS[user_id] = GENAI_CLIENT.chats.create( + model="models/gemini-2.0-flash" + ) + except Exception as e: + logger.error(f"Failed to create AI session for {user_id}: {e}") + return None return AI_SESSIONS[user_id] prompt = """ @@ -58,18 +71,20 @@ def get_ai_session(user_id: int) -> Chat: @Tele.on_message(filters.command("ai") & filters.me) async def ai_cmd(c: Client, m: Message): - if not API_KEY: - return await m.reply("GEMINI_API_KEY not found in config or enviroment. This command will not work without it.") - elif len(m.command) < 2: # type: ignore + if not GENAI_CLIENT or not API_KEY: + return await m.reply("GEMINI_API_KEY not found or AI Client failed to initialize. Please check your config.") + + if len(m.command) < 2: # type: ignore return await m.edit("Usage: `.ai `") - loading = await m.reply("...") + + loading = await m.reply("Thinking...") reply = m.reply_to_message ist_time = datetime.now(ZoneInfo("Asia/Kolkata")) - name = m.from_user.first_name # type: ignore - chat_name = m.chat.full_name # type: ignore - replied_msg = getattr(reply, 'text') if reply else "> SYS: User not replied any message" - replied_msg_user = getattr(reply.from_user, 'first_name') if reply else "> SYS: User not replied any message" + name = m.from_user.first_name if m.from_user else "User" + chat_name = m.chat.title or m.chat.first_name or "Private Chat" + replied_msg = getattr(reply, 'text', None) or getattr(reply, 'caption', None) if reply else "> SYS: User not replied any message" + replied_msg_user = getattr(reply.from_user, 'first_name', "Unknown") if reply else "> SYS: User not replied any message" query = m.text.split(None, 1)[1] # type: ignore @@ -77,11 +92,15 @@ async def ai_cmd(c: Client, m: Message): try: session = get_ai_session(c.me.id) # type: ignore + if not session: + return await loading.edit("Failed to create AI session.") response = await asyncio.to_thread( session.send_message, message ) + + full_text = "" if hasattr(response, "text") and response.text: full_text = response.text else: @@ -104,7 +123,7 @@ async def ai_clear(c: Client, m: Message): uid = c.me.id # type: ignore if AI_SESSIONS.pop(uid, None): - await m.edit("Cleared.") + await m.edit("Cleared AI chat session.") else: await m.edit("No active AI session to clear.") diff --git a/Mods/bot_management.py b/Mods/bot_management.py new file mode 100644 index 0000000..28ba7f1 --- /dev/null +++ b/Mods/bot_management.py @@ -0,0 +1,67 @@ +from Hazel import Tele, SQLClient +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.types import Message +import logging + +logger = logging.getLogger("Mods.bot_management") + +@Tele.bot.on_message(filters.command("login") & filters.private) +async def login_handler(c: Client, m: Message): + # Owner Check + if not Tele.mainClient or not Tele.mainClient.me: + return await m.reply("Userbot is not fully started yet. Please wait.") + + owner_id = Tele.mainClient.me.id + if m.from_user.id != owner_id: + return await m.reply("Only the owner of the main userbot session can use this command.") + + if len(m.command) < 2: + return await m.reply("Usage: `/login `") + + session_string = m.text.split(None, 1)[1] + + # Show loading status + status = await m.reply("Wait... trying to login with this session.") + + try: + # 1. Save to Database + success_db = await SQLClient.add_session(session_string) + if not success_db: + return await status.edit("This session is already in the database.") + + # 2. Dynamically Start Client + success_start = await Tele.add_and_start_client(session_string) + + if success_start: + await status.edit("✅ **Session Logged In Successfully!**\n\nThe new client is now active and will be loaded automatically on next restart.") + else: + # If start fails, maybe remove from DB? Or just notify + await status.edit("⚠️ **Session saved to DB, but failed to start now.**\n\nIt will be attempted again on next bot restart. Check the logs for errors.") + + except Exception as e: + logger.error(f"Error in /login handler: {e}") + await status.edit(f"❌ **Error:** `{e}`") + +@Tele.bot.on_message(filters.command("sessions") & filters.private) +async def sessions_handler(c: Client, m: Message): + # Owner Check + if not Tele.mainClient or not Tele.mainClient.me: + return await m.reply("Userbot is not fully started yet.") + + owner_id = Tele.mainClient.me.id + if m.from_user.id != owner_id: + return await m.reply("Unauthorized.") + + txt = "**Active HazelUB Sessions:**\n\n" + for i, client in enumerate(Tele._allClients): + try: + me = client.me or await client.get_me() + txt += f"{i+1}. **{me.first_name}** (`{me.id}`)\n" + except: + txt += f"{i+1}. *[Unknown/Disconnected]*\n" + + await m.reply(txt) + +MOD_NAME = "Bot Management" +MOD_HELP = "Management commands for the Telegram Bot.\n\nUsage (Private to Bot):\n> /login - Add a new userbot session.\n> /sessions - List all active sessions." diff --git a/Mods/repeater.py b/Mods/repeater.py index 93812c6..49f5b4d 100644 --- a/Mods/repeater.py +++ b/Mods/repeater.py @@ -2,8 +2,7 @@ from pyrogram.client import Client from pyrogram.types import Message from pyrogram import filters -from sqlalchemy.exc import IntegrityError - +from bson import ObjectId # ---------------- Repeat ---------------- @@ -14,7 +13,7 @@ async def repeatFunc(c: Client, m: Message): text = m.text.split() # type: ignore if len(text) < 3: - return await m.reply("Usage: $repeat (minutes) (group_name)") + return await m.reply("Usage: .repeat (minutes) (group_name)") if not m.reply_to_message: return await m.reply("Please reply to a message.") @@ -34,7 +33,7 @@ async def repeatFunc(c: Client, m: Message): ) if not group: - return await m.reply("Group not found. Create one using $rgroup create (name)") + return await m.reply("Group not found. Create one using .rgroup create (name)") try: await SQLClient.create_repeat_message( @@ -42,13 +41,13 @@ async def repeatFunc(c: Client, m: Message): userId=c.me.id, # type: ignore message_id=m.reply_to_message.id, # type: ignore source_chat_id=m.reply_to_message.chat.id, # type: ignore - group_id=group.id + group_id=str(group["_id"]) ) - except IntegrityError: - return await m.reply("Database error while creating repeat task.") + except Exception as e: + return await m.reply(f"Database error while creating repeat task: {e}") return await m.reply( - f"Task created. Repeating every {mins} min in group `{group.name}`." + f"Task created. Repeating every {mins} min in group `{group['name']}`." ) @@ -59,7 +58,7 @@ async def groupCreate(c: Client, m: Message): text = m.text.split(maxsplit=2) # type: ignore if len(text) < 3 or text[1] != "create": - return await m.reply("Usage: $rgroup create (name)") + return await m.reply("Usage: .rgroup create (name)") name = text[2] group = await SQLClient.get_group_by_name(name=name, user_id=c.me.id) # type: ignore @@ -71,7 +70,7 @@ async def groupCreate(c: Client, m: Message): c.me.id # type: ignore ) - return await m.reply(f"Group created: `{group.name}` (id: {group.id})") + return await m.reply(f"Group created: `{group['name']}` (id: {group['_id']})") @Tele.on_message(filters.command('rgroup_add') & filters.me) @@ -79,7 +78,7 @@ async def groupAdd(c: Client, m: Message): text = m.text.split(maxsplit=1) # type: ignore if len(text) < 2: - return await m.reply("Usage: $rgroup_add (group_name)") + return await m.reply("Usage: .rgroup_add (group_name)") group_name = text[1] @@ -91,18 +90,18 @@ async def groupAdd(c: Client, m: Message): if not group: return await m.reply("Group not found.") - chats = await SQLClient.get_group_chats(group.id, c.me.id) # type: ignore + chats = await SQLClient.get_group_chats(str(group["_id"]), c.me.id) # type: ignore for chat_id in chats: if chat_id == m.chat.id: # type: ignore return await m.reply(f'This chat already in group `{group_name}`.') await SQLClient.add_chat_to_group( - group.id, + str(group["_id"]), m.chat.id, # type: ignore c.me.id # type: ignore ) - return await m.reply(f"Chat added to group `{group.name}`.") + return await m.reply(f"Chat added to group `{group['name']}`.") @Tele.on_message(filters.command('rgroup_remove') & filters.me) @@ -110,7 +109,7 @@ async def groupRemove(c: Client, m: Message): text = m.text.split(maxsplit=1) # type: ignore if len(text) < 2: - return await m.reply("Usage: $rgroup_remove (group_name)") + return await m.reply("Usage: .rgroup_remove (group_name)") group_name = text[1] @@ -123,12 +122,12 @@ async def groupRemove(c: Client, m: Message): return await m.reply("Group not found.") await SQLClient.remove_chat_from_group( - group.id, + str(group["_id"]), m.chat.id, # type: ignore c.me.id # type: ignore ) - return await m.reply(f"Chat removed from group `{group.name}`.") + return await m.reply(f"Chat removed from group `{group['name']}`.") @Tele.on_message(filters.command('rgroup_list') & filters.me) @@ -136,7 +135,7 @@ async def groupList(c: Client, m: Message): text = m.text.split(maxsplit=1) # type: ignore if len(text) < 2: - return await m.reply("Usage: $rgroup_list (group_name)") + return await m.reply("Usage: .rgroup_list (group_name)") group_name = text[1] @@ -149,15 +148,15 @@ async def groupList(c: Client, m: Message): return await m.reply("Group not found.") chats = await SQLClient.get_group_chats( - group.id, + str(group["_id"]), c.me.id # type: ignore ) if not chats: return await m.reply("Group is empty.") - msg = f"Chats in `{group.name}`:\n" - msg += "\n".join(str(x) for x in chats) + msg = f"Chats in `{group['name']}`:\n" + msg += "\n".join(f"`{x}`" for x in chats) return await m.reply(msg) @Tele.on_message(filters.command('rgroup_list_all') & filters.me) @@ -172,7 +171,7 @@ async def groupListAll(c: Client, m: Message): msg = "Your Groups:\n\n" for g in groups: - msg += f"- {g.name} (id: {g.id})\n" + msg += f"- {g['name']} (id: `{g['_id']}`)\n" return await m.reply(msg) @@ -184,18 +183,20 @@ async def repeatDelete(c: Client, m: Message): text = m.text.split() # type: ignore if len(text) < 2: - return await m.reply("Usage: $repeat_delete (id)") + return await m.reply("Usage: .repeat_delete (id)") - rid = int(text[1]) - await SQLClient.delete_repeat_message(rid) - return await m.reply("Repeat task deleted. Restart required.") + rid = text[1] + try: + await SQLClient.delete_repeat_message(rid) + return await m.reply("Repeat task deleted. Restart required.") + except Exception as e: + return await m.reply(f"Error: {e}") @Tele.on_message(filters.command('repeat_list') & filters.me) async def repeatList(c: Client, m: Message): rows = await SQLClient.get_repeat_messages() - - rows = [r for r in rows if r.userId == c.me.id] # type: ignore + rows = [r for r in rows if r.get("userId") == c.me.id] # type: ignore if not rows: return await m.reply("No repeat tasks found.") @@ -204,9 +205,9 @@ async def repeatList(c: Client, m: Message): for r in rows: msg += ( - f"ID: {r.id}\n" - f"Every: {r.repeatTime} min\n" - f"Group ID: {r.group_id}\n" + f"ID: `{r.get('_id')}`\n" + f"Every: {r.get('repeatTime')} min\n" + f"Group ID: `{r.get('group_id')}`\n" "----------------------\n" ) @@ -217,7 +218,7 @@ async def pauseAndResumeFunc(c: Client, m: Message): import Hazel.Tasks.messageRepeater as messageRepeater uid = c.me.id # type: ignore if uid not in messageRepeater.events: - return await m.reply("Cannot find this client Hazel.Tasks.messageRepeater.events") + return await m.reply("Cannot find this client in active repeater tasks.") event = messageRepeater.events[uid] if 'resume' in m.command[0].lower(): # type: ignore if event.is_set(): @@ -234,7 +235,7 @@ async def pauseAndResumeFunc(c: Client, m: Message): MOD_HELP = """**Usage:** > .repeat (mins) (group) - Repeat a message. > .rgroup create (name) - Create a group. -> .rgroup_add (group) - Add current chat to group. +> .rgroup_add (group) - Add current chat to group (to receive repeats). > .rgroup_remove (group) - Remove current chat from group. > .rgroup_list (group) - List chats in group. > .rgroup_list_all - List all groups. diff --git a/MultiSessionManagement/telegram.py b/MultiSessionManagement/telegram.py index 27ea80b..5184097 100644 --- a/MultiSessionManagement/telegram.py +++ b/MultiSessionManagement/telegram.py @@ -9,6 +9,8 @@ from .TelegramMethods import Methods import logging +logger = logging.getLogger("Telegram") + class Telegram(Methods, Decorators): def __init__(self, config: tuple) -> None: # ----------- Config--------- @@ -17,6 +19,7 @@ def __init__(self, config: tuple) -> None: self.api_id: int = int(config[1]) self.api_hash: str = config[2] self.bot_token: str = config[0] + self.prefixes: List[str] = config[7] # ----------- Clients ------------ self.bot: Client = Client("HazelUB-Bot") self.mainClient: Client = Client("HazelUB") @@ -27,7 +30,7 @@ def __init__(self, config: tuple) -> None: self._clientPrivileges: Dict[Client, str] = {} self._clientPyTgCalls: Dict[Client, PyTgCalls] = {} - filters.command = partial(filters.command, prefixes=config[7]) # Override filters.command to set defualt prefixes + filters.command = partial(filters.command, prefixes=self.prefixes) # Override filters.command to set defualt prefixes async def create_pyrogram_clients(self) -> None: if len(self.bot_token) > 50: # Bot Client @@ -55,9 +58,20 @@ async def create_pyrogram_clients(self) -> None: self._clientPrivileges[self.mainClient] = "sudo" self._clientPyTgCalls[self.mainClient] = mainClientPyTgCalls - for session in self.othersessions: # Other clients + # Load standard other sessions from config/env + all_other_sessions = list(self.othersessions) + + # Load extra sessions from Database + from Hazel import SQLClient + if SQLClient: + db_sessions = await SQLClient.get_all_sessions() + for s in db_sessions: + if s not in all_other_sessions: + all_other_sessions.append(s) + + for session in all_other_sessions: # Other clients client = Client( - name=f"HazelUB-{session:5}", + name=f"HazelUB-{session[:10]}", session_string=session, api_id=self.api_id, api_hash=self.api_hash @@ -73,26 +87,73 @@ async def create_pyrogram_clients(self) -> None: self._allClients = [self.mainClient, *self.otherClients] self._allPyTgCalls.append(mainClientPyTgCalls) + async def add_and_start_client(self, session_string: str) -> bool: + """Dynamically add and start a new userbot client.""" + try: + # Check if already exists + for c in self._allClients: + if c.session_string == session_string: + return False + + client = Client( + name=f"HazelUB-{session_string[:10]}", + session_string=session_string, + api_id=self.api_id, + api_hash=self.api_hash + ) + + await client.start() + + # Setup privileges and calls + self._clientPrivileges[client] = "user" + clientPyTgCalls = PyTgCalls(client) + self._clientPyTgCalls[client] = clientPyTgCalls + await clientPyTgCalls.start() + + # Update lists + self.otherClients.append(client) + self._allClients.append(client) + self._allPyTgCalls.append(clientPyTgCalls) + + # Join channel + from Hazel import __channel__ + try: + await client.join_chat(__channel__) + except: + pass + + return True + except Exception as e: + logger.error(f"Failed to dynamically start client: {e}") + return False + async def start(self) -> None: # HazelUB await self.bot.start() for client in self._allClients: - await client.start() - pytgcalls = self.getClientPyTgCalls(client) - if pytgcalls: - await pytgcalls.start() + try: + await client.start() + pytgcalls = self.getClientPyTgCalls(client) + if pytgcalls: + await pytgcalls.start() + except Exception as e: + logger.error(f"Failed to start client {client.name}: {e}") from Hazel import __channel__ try: for client in self._allClients: - await client.join_chat(__channel__) + if client.is_connected: + await client.join_chat(__channel__) except: pass async def stop(self) -> None: for client in self._allClients: - await client.stop() + if client.is_connected: + await client.stop() + if self.bot.is_connected: + await self.bot.stop() def getClientById(self, id: int | None = 0, m: Message | None = None) -> Optional[Client]: if m and isinstance(m, Message): @@ -119,7 +180,6 @@ async def is_admin(self, client: Client, chat_id: int, user_id: Optional[int] = ChatMemberStatus.OWNER, ) except Exception as e: - logger = logging.getLogger("Telegram.is_admin") logger.error(f"Error checking admin status: {str(e)}") return False @@ -130,6 +190,5 @@ async def get_chat_member_privileges(self, client: Client, chat_id: int, user_id member = await client.get_chat_member(chat_id, user_id) return member.privileges except Exception as e: - logger = logging.getLogger("Telegram.get_chat_member_privileges") logger.error(f"Error getting chat member privileges: {str(e)}") return None \ No newline at end of file From 91d185bddb73f6f37e874f3c7f78614b8201140f Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 13:23:40 +0530 Subject: [PATCH 07/16] fix: explicit prefixes for bot command handlers --- Mods/bot_management.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mods/bot_management.py b/Mods/bot_management.py index 28ba7f1..471a4bd 100644 --- a/Mods/bot_management.py +++ b/Mods/bot_management.py @@ -6,7 +6,7 @@ logger = logging.getLogger("Mods.bot_management") -@Tele.bot.on_message(filters.command("login") & filters.private) +@Tele.bot.on_message(filters.command("login", prefixes=["/", ""]) & filters.private) async def login_handler(c: Client, m: Message): # Owner Check if not Tele.mainClient or not Tele.mainClient.me: @@ -43,7 +43,7 @@ async def login_handler(c: Client, m: Message): logger.error(f"Error in /login handler: {e}") await status.edit(f"❌ **Error:** `{e}`") -@Tele.bot.on_message(filters.command("sessions") & filters.private) +@Tele.bot.on_message(filters.command("sessions", prefixes=["/", ""]) & filters.private) async def sessions_handler(c: Client, m: Message): # Owner Check if not Tele.mainClient or not Tele.mainClient.me: From 2dd04930f8fca76606acc82adc755b2a876cae66 Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 13:26:49 +0530 Subject: [PATCH 08/16] fix: resolve EntityBoundsInvalid by simplifying bot message formatting --- Mods/bot_management.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Mods/bot_management.py b/Mods/bot_management.py index 471a4bd..679b7ca 100644 --- a/Mods/bot_management.py +++ b/Mods/bot_management.py @@ -6,7 +6,7 @@ logger = logging.getLogger("Mods.bot_management") -@Tele.bot.on_message(filters.command("login", prefixes=["/", ""]) & filters.private) +@Tele.bot.on_message(filters.command("login", prefixes=["/"]) & filters.private) async def login_handler(c: Client, m: Message): # Owner Check if not Tele.mainClient or not Tele.mainClient.me: @@ -17,7 +17,7 @@ async def login_handler(c: Client, m: Message): return await m.reply("Only the owner of the main userbot session can use this command.") if len(m.command) < 2: - return await m.reply("Usage: `/login `") + return await m.reply("Usage: /login session_string") session_string = m.text.split(None, 1)[1] @@ -34,16 +34,15 @@ async def login_handler(c: Client, m: Message): success_start = await Tele.add_and_start_client(session_string) if success_start: - await status.edit("✅ **Session Logged In Successfully!**\n\nThe new client is now active and will be loaded automatically on next restart.") + await status.edit("✅ **Session Logged In Successfully!**\n\nThe new client is now active.") else: - # If start fails, maybe remove from DB? Or just notify - await status.edit("⚠️ **Session saved to DB, but failed to start now.**\n\nIt will be attempted again on next bot restart. Check the logs for errors.") + await status.edit("⚠️ **Session saved to DB, but failed to start now.**\n\nIt will be attempted again on next bot restart.") except Exception as e: logger.error(f"Error in /login handler: {e}") await status.edit(f"❌ **Error:** `{e}`") -@Tele.bot.on_message(filters.command("sessions", prefixes=["/", ""]) & filters.private) +@Tele.bot.on_message(filters.command("sessions", prefixes=["/"]) & filters.private) async def sessions_handler(c: Client, m: Message): # Owner Check if not Tele.mainClient or not Tele.mainClient.me: @@ -56,7 +55,9 @@ async def sessions_handler(c: Client, m: Message): txt = "**Active HazelUB Sessions:**\n\n" for i, client in enumerate(Tele._allClients): try: - me = client.me or await client.get_me() + # me info might be cached + me = client.me + if not me: me = await client.get_me() txt += f"{i+1}. **{me.first_name}** (`{me.id}`)\n" except: txt += f"{i+1}. *[Unknown/Disconnected]*\n" From 9cea671c67763a23b2fc1685afa4bdf5eeef7a27 Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 13:47:48 +0530 Subject: [PATCH 09/16] fix: enable dynamic command registration for runtime-added clients --- MultiSessionManagement/decorators.py | 14 +++++++++++++- MultiSessionManagement/telegram.py | 8 ++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/MultiSessionManagement/decorators.py b/MultiSessionManagement/decorators.py index 2a1b4f1..738246f 100644 --- a/MultiSessionManagement/decorators.py +++ b/MultiSessionManagement/decorators.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Tuple, Any if TYPE_CHECKING: from MultiSessionManagement.telegram import Telegram @@ -6,8 +6,16 @@ Telegram = None class Decorators: + # Storage for dynamic registration + _message_handlers: List[Tuple[Any, int, Any]] = [] # (filters, group, func) + _update_handlers: List[Tuple[Any, Any]] = [] # (args, func) + def on_message(self: Telegram, filters=None, group=0): # type: ignore def decorator(func): + # Store for later + self._message_handlers.append((filters, group, func)) + + # Register to current clients for i in self._allClients: i.on_message(filters, group=group)(func) return func @@ -15,6 +23,10 @@ def decorator(func): def on_update(self: Telegram, *args): # type: ignore def decorator(func): + # Store for later + self._update_handlers.append((args, func)) + + # Register to current clients for i in self._allPyTgCalls: i.on_update(*args)(func) return func diff --git a/MultiSessionManagement/telegram.py b/MultiSessionManagement/telegram.py index 5184097..4624985 100644 --- a/MultiSessionManagement/telegram.py +++ b/MultiSessionManagement/telegram.py @@ -102,12 +102,20 @@ async def add_and_start_client(self, session_string: str) -> bool: api_hash=self.api_hash ) + # Apply stored handlers + for filters, group, func in self._message_handlers: + client.on_message(filters, group=group)(func) + await client.start() # Setup privileges and calls self._clientPrivileges[client] = "user" clientPyTgCalls = PyTgCalls(client) self._clientPyTgCalls[client] = clientPyTgCalls + + for args, func in self._update_handlers: + clientPyTgCalls.on_update(*args)(func) + await clientPyTgCalls.start() # Update lists From fe86cced4f66108fa3c9aa658c2ef4587feeb0ba Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 13:56:08 +0530 Subject: [PATCH 10/16] feat: add AFK plugin with persistent storage and multi-client support --- Database/Methods/__init__.py | 4 +- Database/Methods/afkMethods.py | 20 +++++++ Database/mongo_client.py | 3 +- Hazel/__init__.py | 2 +- Mods/afk.py | 101 +++++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 Database/Methods/afkMethods.py create mode 100644 Mods/afk.py diff --git a/Database/Methods/__init__.py b/Database/Methods/__init__.py index f9fe9a1..628e9fb 100644 --- a/Database/Methods/__init__.py +++ b/Database/Methods/__init__.py @@ -1,8 +1,10 @@ from .repeatMethods import RepeatMethods from .sessionMethods import SessionMethods +from .afkMethods import AFKMethods class Methods( RepeatMethods, - SessionMethods + SessionMethods, + AFKMethods ): pass \ No newline at end of file diff --git a/Database/Methods/afkMethods.py b/Database/Methods/afkMethods.py new file mode 100644 index 0000000..7361dce --- /dev/null +++ b/Database/Methods/afkMethods.py @@ -0,0 +1,20 @@ +from typing import Optional, Dict, Any +import time + +class AFKMethods: + async def set_afk(self, user_id: int, reason: str): + """Enable AFK status for a user.""" + await self.db["afk"].update_one( + {"user_id": user_id}, + {"$set": {"reason": reason, "time": time.time()}}, + upsert=True + ) + + async def get_afk(self, user_id: int) -> Optional[Dict[str, Any]]: + """Retrieve AFK information for a user.""" + return await self.db["afk"].find_one({"user_id": user_id}) + + async def remove_afk(self, user_id: int) -> bool: + """Disable AFK status for a user.""" + result = await self.db["afk"].delete_one({"user_id": user_id}) + return result.deleted_count > 0 diff --git a/Database/mongo_client.py b/Database/mongo_client.py index 0ae80a5..eb0ec0d 100644 --- a/Database/mongo_client.py +++ b/Database/mongo_client.py @@ -1,8 +1,9 @@ from motor.motor_asyncio import AsyncIOMotorClient from .Methods.repeatMethods import RepeatMethods from .Methods.sessionMethods import SessionMethods +from .Methods.afkMethods import AFKMethods -class MongoClient(RepeatMethods, SessionMethods): +class MongoClient(RepeatMethods, SessionMethods, AFKMethods): def __init__(self, mongo_url: str): self._client = AsyncIOMotorClient(mongo_url) self.db = self._client["UBdrag"] diff --git a/Hazel/__init__.py b/Hazel/__init__.py index 2c1ac7c..e4eff45 100644 --- a/Hazel/__init__.py +++ b/Hazel/__init__.py @@ -25,4 +25,4 @@ ) __version__ = "02.2026" -__channel__ = "DevsBase" +__channel__ = "dragbots" diff --git a/Mods/afk.py b/Mods/afk.py new file mode 100644 index 0000000..7f3681a --- /dev/null +++ b/Mods/afk.py @@ -0,0 +1,101 @@ +from Hazel import Tele, SQLClient +from pyrogram import filters +from pyrogram.client import Client +from pyrogram.types import ( + Message, + InlineKeyboardMarkup, + InlineKeyboardButton, + InlineQuery, + InlineQueryResultArticle, + InputTextMessageContent +) +import time +import logging + +logger = logging.getLogger("Mods.afk") + +def get_readable_time(seconds: int) -> str: + # Weeks support for the user's requested style + weeks, seconds = divmod(seconds, 604800) + days, seconds = divmod(seconds, 86400) + hours, seconds = divmod(seconds, 3600) + minutes, seconds = divmod(seconds, 60) + + parts = [] + if weeks: parts.append(f"{int(weeks)}w") + if days: parts.append(f"{int(days)}d") + if hours: parts.append(f"{int(hours)}h") + if minutes: parts.append(f"{int(minutes)}m") + parts.append(f"{int(seconds)}s") + + return ":".join(parts) + +@Tele.on_message(filters.command("afk") & filters.me) +async def afk_cmd(c: Client, m: Message): + reason = "Away from keyboard" + if len(m.command) > 1: + reason = m.text.split(None, 1)[1] + + await SQLClient.set_afk(c.me.id, reason) + + bot_username = (await Tele.bot.get_me()).username + results = await c.get_inline_bot_results(bot_username, f"afk_set {reason}") + if results.results: + await c.send_inline_bot_result( + m.chat.id, + results.query_id, + results.results[0].id + ) + await m.delete() + +# --- AFK Cancellation Listener --- +@Tele.on_message(filters.me & ~filters.command(["afk", "ping", "uptime"])) +async def afk_stop_listener(c: Client, m: Message): + afk_data = await SQLClient.get_afk(c.me.id) + if afk_data: + duration = get_readable_time(int(time.time() - afk_data["time"])) + await SQLClient.remove_afk(c.me.id) + await m.reply(f"✅ **I'm back!**\nI was AFK for `{duration}`.", quote=True) + +# --- AFK Mention/Reply Listener --- +@Tele.on_message((filters.mentioned | filters.reply) & ~filters.me & filters.incoming) +async def afk_mention_listener(c: Client, m: Message): + target_user_id = None + + # Check if mentioned + if filters.mentioned(c, m): + target_user_id = c.me.id + # Check if reply to me + elif m.reply_to_message and m.reply_to_message.from_user and m.reply_to_message.from_user.id == c.me.id: + target_user_id = c.me.id + + if target_user_id: + afk_data = await SQLClient.get_afk(target_user_id) + if afk_data: + duration = get_readable_time(int(time.time() - afk_data["time"])) + reason = afk_data["reason"] + await m.reply( + f"👤 **{c.me.first_name} is AFK.**\n" + f"📝 **Reason:** `{reason}`\n" + f"🕒 **Away for:** `{duration}`", + quote=True + ) + +# --- Inline Handlers --- + +@Tele.bot.on_inline_query(filters.regex(r"^afk_set (.*)")) +async def afk_inline(c: Client, q: InlineQuery): + reason = q.matches[0].group(1) + await q.answer([ + InlineQueryResultArticle( + title="Go AFK", + description=f"Reason: {reason}", + input_message_content=InputTextMessageContent( + f"💤 **I am now AFK.**\n" + f"📝 **Reason:** `{reason}`" + ) + ) + ], cache_time=0) + +MOD_NAME = "AFK" +MOD_HELP = "Set yourself AFK.\n\nUsage:\n> .afk [reason] - Go AFK. Any message you send will turn it off." From 6f75a26f2d9481d83678d02e5b73abaf0353a4ba Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 13:59:45 +0530 Subject: [PATCH 11/16] fix: resolve AFK plugin warnings and restrict auto-replies to tags --- Mods/afk.py | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/Mods/afk.py b/Mods/afk.py index 7f3681a..32c06f0 100644 --- a/Mods/afk.py +++ b/Mods/afk.py @@ -55,31 +55,20 @@ async def afk_stop_listener(c: Client, m: Message): if afk_data: duration = get_readable_time(int(time.time() - afk_data["time"])) await SQLClient.remove_afk(c.me.id) - await m.reply(f"✅ **I'm back!**\nI was AFK for `{duration}`.", quote=True) + await m.reply(f"✅ **I'm back!**\nI was AFK for `{duration}`.") -# --- AFK Mention/Reply Listener --- -@Tele.on_message((filters.mentioned | filters.reply) & ~filters.me & filters.incoming) +# --- AFK Mention Listener --- +@Tele.on_message(filters.incoming & filters.mentioned & ~filters.me) async def afk_mention_listener(c: Client, m: Message): - target_user_id = None - - # Check if mentioned - if filters.mentioned(c, m): - target_user_id = c.me.id - # Check if reply to me - elif m.reply_to_message and m.reply_to_message.from_user and m.reply_to_message.from_user.id == c.me.id: - target_user_id = c.me.id - - if target_user_id: - afk_data = await SQLClient.get_afk(target_user_id) - if afk_data: - duration = get_readable_time(int(time.time() - afk_data["time"])) - reason = afk_data["reason"] - await m.reply( - f"👤 **{c.me.first_name} is AFK.**\n" - f"📝 **Reason:** `{reason}`\n" - f"🕒 **Away for:** `{duration}`", - quote=True - ) + afk_data = await SQLClient.get_afk(c.me.id) + if afk_data: + duration = get_readable_time(int(time.time() - afk_data["time"])) + reason = afk_data["reason"] + await m.reply( + f"👤 **{c.me.first_name} is AFK.**\n" + f"📝 **Reason:** `{reason}`\n" + f"🕒 **Away for:** `{duration}`" + ) # --- Inline Handlers --- From 4abbc9060c346e0dff848fc07866f273aa78de47 Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 14:02:45 +0530 Subject: [PATCH 12/16] fix: refine AFK trigger to handle both mentions and direct replies --- Mods/afk.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Mods/afk.py b/Mods/afk.py index 32c06f0..90b82d0 100644 --- a/Mods/afk.py +++ b/Mods/afk.py @@ -57,9 +57,22 @@ async def afk_stop_listener(c: Client, m: Message): await SQLClient.remove_afk(c.me.id) await m.reply(f"✅ **I'm back!**\nI was AFK for `{duration}`.") -# --- AFK Mention Listener --- -@Tele.on_message(filters.incoming & filters.mentioned & ~filters.me) +# --- AFK Mention/Reply Listener --- +@Tele.on_message(filters.incoming & ~filters.me) async def afk_mention_listener(c: Client, m: Message): + # We check if: + # 1. The message mentions the user (c.me.id) + # 2. The message is a reply to the user (c.me.id) + + is_mentioned = False + if m.mentioned: + is_mentioned = True + elif m.reply_to_message and m.reply_to_message.from_user and m.reply_to_message.from_user.id == c.me.id: + is_mentioned = True + + if not is_mentioned: + return + afk_data = await SQLClient.get_afk(c.me.id) if afk_data: duration = get_readable_time(int(time.time() - afk_data["time"])) From 0aeaee9a6abd72663903fff481045435286b8ca9 Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 14:09:09 +0530 Subject: [PATCH 13/16] fix: restore .help command by refactoring handler registration and prefix patching --- MultiSessionManagement/decorators.py | 22 ++++++++++++++-------- MultiSessionManagement/telegram.py | 25 +++++++++++++++++++++---- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/MultiSessionManagement/decorators.py b/MultiSessionManagement/decorators.py index 738246f..704e218 100644 --- a/MultiSessionManagement/decorators.py +++ b/MultiSessionManagement/decorators.py @@ -6,28 +6,34 @@ Telegram = None class Decorators: - # Storage for dynamic registration - _message_handlers: List[Tuple[Any, int, Any]] = [] # (filters, group, func) - _update_handlers: List[Tuple[Any, Any]] = [] # (args, func) - def on_message(self: Telegram, filters=None, group=0): # type: ignore def decorator(func): + # Ensure instance-level list exists + if not hasattr(self, "_message_handlers"): + self._message_handlers = [] + # Store for later self._message_handlers.append((filters, group, func)) # Register to current clients - for i in self._allClients: - i.on_message(filters, group=group)(func) + if hasattr(self, "_allClients"): + for i in self._allClients: + i.on_message(filters, group=group)(func) return func return decorator def on_update(self: Telegram, *args): # type: ignore def decorator(func): + # Ensure instance-level list exists + if not hasattr(self, "_update_handlers"): + self._update_handlers = [] + # Store for later self._update_handlers.append((args, func)) # Register to current clients - for i in self._allPyTgCalls: - i.on_update(*args)(func) + if hasattr(self, "_allPyTgCalls"): + for i in self._allPyTgCalls: + i.on_update(*args)(func) return func return decorator \ No newline at end of file diff --git a/MultiSessionManagement/telegram.py b/MultiSessionManagement/telegram.py index 4624985..8131877 100644 --- a/MultiSessionManagement/telegram.py +++ b/MultiSessionManagement/telegram.py @@ -29,9 +29,26 @@ def __init__(self, config: tuple) -> None: self._allPyTgCalls: List[PyTgCalls] = [] self._clientPrivileges: Dict[Client, str] = {} self._clientPyTgCalls: Dict[Client, PyTgCalls] = {} + + # Initializing handler lists on the instance + self._message_handlers = [] + self._update_handlers = [] - filters.command = partial(filters.command, prefixes=self.prefixes) # Override filters.command to set defualt prefixes + # Safer filters.command override + self._patch_filters_command() + def _patch_filters_command(self): + """Standardize prefixes for all command filters.""" + if not hasattr(filters, "_original_command"): + filters._original_command = filters.command + + def custom_command(commands: str | List[str], prefixes: str | List[str] = None, case_sensitive: bool = False): + if prefixes is None: + prefixes = self.prefixes + return filters._original_command(commands, prefixes, case_sensitive) + + filters.command = custom_command + async def create_pyrogram_clients(self) -> None: if len(self.bot_token) > 50: # Bot Client self.bot = Client( @@ -103,8 +120,8 @@ async def add_and_start_client(self, session_string: str) -> bool: ) # Apply stored handlers - for filters, group, func in self._message_handlers: - client.on_message(filters, group=group)(func) + for f, group, func in self._message_handlers: + client.on_message(f, group=group)(func) await client.start() @@ -115,7 +132,7 @@ async def add_and_start_client(self, session_string: str) -> bool: for args, func in self._update_handlers: clientPyTgCalls.on_update(*args)(func) - + await clientPyTgCalls.start() # Update lists From edc5d535ebf023793b106c70048cb5f4d7878de6 Mon Sep 17 00:00:00 2001 From: taslim19 Date: Sat, 21 Feb 2026 17:37:32 +0530 Subject: [PATCH 14/16] feat: implement multi-level sudo system and restore inline aesthetics --- Database/Methods/sudoMethods.py | 88 +++++++++++++++++ Database/mongo_client.py | 3 +- Mods/admins.py | 2 +- Mods/afk.py | 14 +-- Mods/ai.py | 24 +++-- Mods/bot_management.py | 6 +- Mods/calculator.py | 5 +- Mods/del.py | 2 +- Mods/help.py | 67 +++++++------ Mods/music.py | 14 +-- Mods/ping.py | 137 ++++++++++++++++++--------- Mods/purge.py | 14 ++- Mods/repeater.py | 45 +++++++-- Mods/sudo.py | 76 +++++++++++++++ Mods/web-tools.py | 5 +- MultiSessionManagement/decorators.py | 89 ++++++++++++++--- MultiSessionManagement/telegram.py | 90 ++++++++---------- Setup/main.py | 3 + Setup/utils.py | 14 ++- requirements.txt | 3 +- 20 files changed, 514 insertions(+), 187 deletions(-) create mode 100644 Database/Methods/sudoMethods.py create mode 100644 Mods/sudo.py diff --git a/Database/Methods/sudoMethods.py b/Database/Methods/sudoMethods.py new file mode 100644 index 0000000..b6ecb03 --- /dev/null +++ b/Database/Methods/sudoMethods.py @@ -0,0 +1,88 @@ +from typing import List, Optional +import logging + +logger = logging.getLogger("Hazel.SudoMethods") + +class SudoMethods: + # MongoDB collections are initialized in MongoClient + # We use Redis for fast lookup: "sudo_list" and "fsudo_list" (Sets) + + async def add_sudo(self, user_id: int, level: str = "sudo"): + """ + level can be 'sudo' or 'fsudo' + """ + user_id = int(user_id) + # 1. Store in MongoDB + collection = self.db["sudo_users"] + await collection.update_one( + {"user_id": user_id}, + {"$set": {"level": level}}, + upsert=True + ) + + # 2. Update Redis + from Hazel import Redis + if Redis: + set_name = f"{level}_list" + await Redis.redis.sadd(set_name, str(user_id)) + + logger.info(f"Added user {user_id} as {level}") + + async def remove_sudo(self, user_id: int): + user_id = int(user_id) + # 1. Remove from MongoDB + collection = self.db["sudo_users"] + await collection.delete_one({"user_id": user_id}) + + # 2. Remove from Redis + from Hazel import Redis + if Redis: + await Redis.redis.srem("sudo_list", str(user_id)) + await Redis.redis.srem("fsudo_list", str(user_id)) + + logger.info(f"Removed user {user_id} from sudo list") + + async def get_all_sudo(self) -> List[dict]: + collection = self.db["sudo_users"] + return await collection.find({}).to_list(length=None) + + async def is_sudo(self, user_id: int) -> bool: + user_id = int(user_id) + from Hazel import Redis + if Redis: + # Check both as strings (we store them as strings in Redis) + res1 = await Redis.redis.sismember("sudo_list", str(user_id)) + res2 = await Redis.redis.sismember("fsudo_list", str(user_id)) + is_it = bool(res1 or res2) + return is_it + + collection = self.db["sudo_users"] + res = await collection.find_one({"user_id": user_id}) + return res is not None + + async def is_fsudo(self, user_id: int) -> bool: + user_id = int(user_id) + from Hazel import Redis + if Redis: + res = await Redis.redis.sismember("fsudo_list", str(user_id)) + return bool(res) + + collection = self.db["sudo_users"] + res = await collection.find_one({"user_id": user_id, "level": "fsudo"}) + return res is not None + + async def reload_sudo_cache(self): + """Load all sudo users from Mongo into Redis.""" + from Hazel import Redis + if not Redis: return + + # Clear existing + await Redis.redis.delete("sudo_list") + await Redis.redis.delete("fsudo_list") + + all_users = await self.get_all_sudo() + for u in all_users: + level = u.get("level", "sudo") + await Redis.redis.sadd(f"{level}_list", str(u["user_id"])) + + logger.info(f"Reloaded {len(all_users)} sudo users into Redis cache") diff --git a/Database/mongo_client.py b/Database/mongo_client.py index eb0ec0d..0ddafd2 100644 --- a/Database/mongo_client.py +++ b/Database/mongo_client.py @@ -2,8 +2,9 @@ from .Methods.repeatMethods import RepeatMethods from .Methods.sessionMethods import SessionMethods from .Methods.afkMethods import AFKMethods +from .Methods.sudoMethods import SudoMethods -class MongoClient(RepeatMethods, SessionMethods, AFKMethods): +class MongoClient(RepeatMethods, SessionMethods, AFKMethods, SudoMethods): def __init__(self, mongo_url: str): self._client = AsyncIOMotorClient(mongo_url) self.db = self._client["UBdrag"] diff --git a/Mods/admins.py b/Mods/admins.py index 36de5a3..5aab9d8 100644 --- a/Mods/admins.py +++ b/Mods/admins.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) -@Tele.on_message(filters.command(["ban", 'unban', 'kick']) & filters.me & filters.group) +@Tele.on_message(filters.command(["ban", 'unban', 'kick']) & filters.group, fsudo=True) async def banFunc(c: Client, m: Message): ban_or_unban_or_kick = m.command[0] # type: ignore if len(m.command) < 2 and not m.reply_to_message: # type: ignore diff --git a/Mods/afk.py b/Mods/afk.py index 90b82d0..eb67566 100644 --- a/Mods/afk.py +++ b/Mods/afk.py @@ -12,7 +12,7 @@ import time import logging -logger = logging.getLogger("Mods.afk") +logger = logging.getLogger("Hazel.AFK") def get_readable_time(seconds: int) -> str: # Weeks support for the user's requested style @@ -49,21 +49,21 @@ async def afk_cmd(c: Client, m: Message): await m.delete() # --- AFK Cancellation Listener --- -@Tele.on_message(filters.me & ~filters.command(["afk", "ping", "uptime"])) +@Tele.on_message(filters.me & ~filters.command(["afk", "ping", "uptime", "help"]), group=-1) async def afk_stop_listener(c: Client, m: Message): afk_data = await SQLClient.get_afk(c.me.id) if afk_data: duration = get_readable_time(int(time.time() - afk_data["time"])) await SQLClient.remove_afk(c.me.id) await m.reply(f"✅ **I'm back!**\nI was AFK for `{duration}`.") + + # Crucial: Let other handlers (like .help) process this message too + m.continue_propagation() # --- AFK Mention/Reply Listener --- -@Tele.on_message(filters.incoming & ~filters.me) +@Tele.on_message(filters.incoming & ~filters.me, group=1, allow_all=True) async def afk_mention_listener(c: Client, m: Message): - # We check if: - # 1. The message mentions the user (c.me.id) - # 2. The message is a reply to the user (c.me.id) - + m.continue_propagation() is_mentioned = False if m.mentioned: is_mentioned = True diff --git a/Mods/ai.py b/Mods/ai.py index 158e585..459bbb6 100644 --- a/Mods/ai.py +++ b/Mods/ai.py @@ -40,7 +40,7 @@ def get_ai_session(user_id: int) -> Optional[Chat]: if user_id not in AI_SESSIONS: try: AI_SESSIONS[user_id] = GENAI_CLIENT.chats.create( - model="models/gemini-2.0-flash" + model="models/gemini-1.5-flash" ) except Exception as e: logger.error(f"Failed to create AI session for {user_id}: {e}") @@ -69,15 +69,18 @@ def get_ai_session(user_id: int) -> Optional[Chat]: User Prompt: {} """ -@Tele.on_message(filters.command("ai") & filters.me) +@Tele.on_message(filters.command("ai"), sudo=True) async def ai_cmd(c: Client, m: Message): if not GENAI_CLIENT or not API_KEY: return await m.reply("GEMINI_API_KEY not found or AI Client failed to initialize. Please check your config.") if len(m.command) < 2: # type: ignore - return await m.edit("Usage: `.ai `") + return await m.reply("Usage: `.ai `") loading = await m.reply("Thinking...") + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass reply = m.reply_to_message ist_time = datetime.now(ZoneInfo("Asia/Kolkata")) @@ -115,17 +118,24 @@ async def ai_cmd(c: Client, m: Message): except Exception as e: logger.error(f"Gemini AI Error: {e}") - await loading.edit(f"Error: `{e}`") + if "429" in str(e) or "RESOURCE_EXHAUSTED" in str(e): + await loading.edit("❌ **Quota Exhausted!**\nYour Gemini API free tier has reached its daily limit. Please try again tomorrow or use a different key.") + else: + await loading.edit(f"Error: `{e}`") -@Tele.on_message(filters.command("aiclr") & filters.me) +@Tele.on_message(filters.command("aiclr"), sudo=True) async def ai_clear(c: Client, m: Message): uid = c.me.id # type: ignore if AI_SESSIONS.pop(uid, None): - await m.edit("Cleared AI chat session.") + await m.reply("Cleared AI chat session.") else: - await m.edit("No active AI session to clear.") + await m.reply("No active AI session to clear.") + + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass MOD_NAME = "AI" diff --git a/Mods/bot_management.py b/Mods/bot_management.py index 679b7ca..0440f9c 100644 --- a/Mods/bot_management.py +++ b/Mods/bot_management.py @@ -13,8 +13,8 @@ async def login_handler(c: Client, m: Message): return await m.reply("Userbot is not fully started yet. Please wait.") owner_id = Tele.mainClient.me.id - if m.from_user.id != owner_id: - return await m.reply("Only the owner of the main userbot session can use this command.") + if m.from_user.id != owner_id and not await SQLClient.is_fsudo(m.from_user.id): + return await m.reply("Only the owner or FSudo users can use this command.") if len(m.command) < 2: return await m.reply("Usage: /login session_string") @@ -49,7 +49,7 @@ async def sessions_handler(c: Client, m: Message): return await m.reply("Userbot is not fully started yet.") owner_id = Tele.mainClient.me.id - if m.from_user.id != owner_id: + if m.from_user.id != owner_id and not await SQLClient.is_fsudo(m.from_user.id): return await m.reply("Unauthorized.") txt = "**Active HazelUB Sessions:**\n\n" diff --git a/Mods/calculator.py b/Mods/calculator.py index f548035..a7d3065 100644 --- a/Mods/calculator.py +++ b/Mods/calculator.py @@ -61,8 +61,11 @@ def evaluate(node): return evaluate(node) -@Tele.on_message(filters.regex(r'^//') & filters.me) +@Tele.on_message(filters.regex(r'^//'), sudo=True) async def calculateFunc(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass exp = m.text.strip() # type: ignore if not exp.startswith('//'): diff --git a/Mods/del.py b/Mods/del.py index 11a20ae..28024b7 100644 --- a/Mods/del.py +++ b/Mods/del.py @@ -3,7 +3,7 @@ import pyrogram.types as types from Hazel import Tele -@Tele.on_message(filters.command(['d','del','delete']) & filters.me) +@Tele.on_message(filters.command(['d','del','delete']), fsudo=True) async def delCommand(client: Client, m: types.Message): if (not m.reply_to_message): return await m.reply('Reply to a message.') diff --git a/Mods/help.py b/Mods/help.py index 9e9108f..c2491d6 100644 --- a/Mods/help.py +++ b/Mods/help.py @@ -1,23 +1,24 @@ -from Hazel import Tele +import Hazel from pyrogram.client import Client from pyrogram import filters from pyrogram.types import ( Message, InlineKeyboardMarkup, InlineKeyboardButton, - CallbackQuery, + CallbackQuery, InlineQuery, InlineQueryResultArticle, InputTextMessageContent ) -from pyrogram.errors import BadRequest import os import importlib import logging +import sys -logger = logging.getLogger(__name__) +logger = logging.getLogger("Hazel.Help") MODS_HELP = {} +BOT_INFO = {"username": None} def load_mods_help(): global MODS_HELP @@ -30,8 +31,6 @@ def load_mods_help(): mod_name_id = file[:-3] module_path = f"Mods.{mod_name_id}" try: - # Use sys.modules to avoid re-importing if already loaded - import sys if module_path in sys.modules: module = sys.modules[module_path] else: @@ -45,15 +44,12 @@ def load_mods_help(): logger.error(f"Error loading help for {module_path}: {e}") return MODS_HELP - def get_help_markup(page_num=0): mods = load_mods_help() mod_names = sorted(mods.keys()) page_size = 10 total_pages = (len(mod_names) + page_size - 1) // page_size - - if total_pages == 0: - return None, 0 + if total_pages == 0: return None, 0 if page_num < 0: page_num = 0 if page_num >= total_pages: page_num = total_pages - 1 @@ -72,41 +68,52 @@ def get_help_markup(page_num=0): nav = [] if page_num > 0: nav.append(InlineKeyboardButton("⬅️ Prev", callback_data=f"hpage_{page_num-1}")) - nav.append(InlineKeyboardButton(f"Page {page_num+1}/{total_pages}", callback_data="none")) - if page_num < total_pages - 1: nav.append(InlineKeyboardButton("Next ➡️", callback_data=f"hpage_{page_num+1}")) - buttons.append(nav) + if nav: + buttons.append(nav) return InlineKeyboardMarkup(buttons), len(mods) -@Tele.on_message(filters.command("help") & filters.me) +@Hazel.Tele.on_message(filters.command("help"), sudo=True) async def help_userbot(c: Client, m: Message): + global BOT_INFO try: - bot_username = str((await Tele.bot.get_me()).username) + if not BOT_INFO["username"]: + me = Hazel.Tele.bot.me or await Hazel.Tele.bot.get_me() + BOT_INFO["username"] = str(me.username) + + bot_username = BOT_INFO["username"] results = await c.get_inline_bot_results(bot_username, "help") if results.results: await c.send_inline_bot_result( - m.chat.id, # type: ignore + m.chat.id, results.query_id, results.results[0].id ) - await m.delete() + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass else: - await m.edit("No results from bot. Make sure inline mode is enabled.") + await m.reply("❌ **Error:** No results from the help bot. Ensure inline mode is ON.") except Exception as e: - if 'CHAT_SEND_INLINE_FORBIDDEN' in str(e): - return await m.reply("Sending inline messages is not allowed in this chat.") - elif "BOT_INLINE_DISABLED" in str(e): - return await m.reply(f'Please enable inline mode for @{Tele.bot.me.username} in @BotFather') # type: ignore + error_msg = str(e) + if 'CHAT_SEND_INLINE_FORBIDDEN' in error_msg: + markup, count = get_help_markup(0) + await m.reply(f"**HazelUB Help Menu**\n\nTotal Modules: {count}\n\n*Note: Inline is forbidden here.*", reply_markup=markup) + elif "BOT_INLINE_DISABLED" in error_msg: + await m.reply(f'❌ **Inline Disabled!**\nPlease enable inline mode for bot in @BotFather') else: - logging.error(f"Error while sending help menu: {e}") - await m.edit(f"**HazelUB Help**\n\nError: {e}\nMake sure inline mode is enabled for the bot.") - + logger.error(f"Error in help command: {e}") + await m.reply(f"⚠️ **HazelUB Help Error**\n\n`{e}`") + + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass -@Tele.bot.on_inline_query(filters.regex("help")) +@Hazel.Tele.bot.on_inline_query(filters.regex("^help$")) async def help_inline(c: Client, q: InlineQuery): markup, count = get_help_markup(0) if not markup: @@ -126,7 +133,7 @@ async def help_inline(c: Client, q: InlineQuery): ) ], cache_time=1) -@Tele.bot.on_callback_query(filters.regex(r"^hpage_(\d+)$")) +@Hazel.Tele.bot.on_callback_query(filters.regex(r"^hpage_(\d+)$")) async def help_page_cb(c: Client, q: CallbackQuery): page_num = int(q.matches[0].group(1)) markup, count = get_help_markup(page_num) @@ -135,7 +142,7 @@ async def help_page_cb(c: Client, q: CallbackQuery): reply_markup=markup # type: ignore ) -@Tele.bot.on_callback_query(filters.regex(r"^hmod_(.*)_(\d+)$")) +@Hazel.Tele.bot.on_callback_query(filters.regex(r"^hmod_(.*)_(\d+)$")) async def help_mod_cb(c: Client, q: CallbackQuery): mod_name = q.matches[0].group(1) page_num = int(q.matches[0].group(2)) @@ -148,9 +155,9 @@ async def help_mod_cb(c: Client, q: CallbackQuery): reply_markup=InlineKeyboardMarkup(buttons) # type: ignore ) -@Tele.bot.on_callback_query(filters.regex("none")) +@Hazel.Tele.bot.on_callback_query(filters.regex("none")) async def none_cb(c, q): await q.answer() MOD_NAME = "Help" -MOD_HELP = "Shows this help menu.\n\nUsage: `.help`" +MOD_HELP = "Shows the bot help menu.\n\nUsage: `.help`" diff --git a/Mods/music.py b/Mods/music.py index b0196d8..f4237ce 100644 --- a/Mods/music.py +++ b/Mods/music.py @@ -322,7 +322,7 @@ async def stream_end_handler(c: PyTgCalls, update: Update) -> None: await play_next(ub_client.me.id, update.chat_id, c) return -@Tele.on_message(filters.command('play') & filters.me) +@Tele.on_message(filters.command('play'), sudo=True) async def play_command(c: Client, m: Message) -> None: if not m.chat or not m.command or m.chat.id is None or not c.me: return @@ -427,7 +427,7 @@ async def play_command(c: Client, m: Message) -> None: if loading: await loading.edit(text) else: await m.reply(text) -@Tele.on_message(filters.command(['skip', 'next']) & filters.me) +@Tele.on_message(filters.command(['skip', 'next']), sudo=True) async def skip_cmd_handler(c: Client, m: Message) -> None: if not m.chat or m.chat.id is None or not c.me: return chat_id: int = m.chat.id @@ -437,7 +437,7 @@ async def skip_cmd_handler(c: Client, m: Message) -> None: else: await m.reply("Nothing is playing to skip.") -@Tele.on_message(filters.command('mstop') & filters.me) +@Tele.on_message(filters.command('mstop'), sudo=True) async def stop_cmd_handler(c: Client, m: Message) -> None: if not m.chat or m.chat.id is None or not c.me: return chat_id: int = m.chat.id @@ -447,7 +447,7 @@ async def stop_cmd_handler(c: Client, m: Message) -> None: else: await m.reply("Not in voice chat.") -@Tele.on_message(filters.command('pause') & filters.me) +@Tele.on_message(filters.command('pause'), sudo=True) async def pause_cmd_handler(c: Client, m: Message) -> None: if not m.chat or m.chat.id is None or not c.me: return chat_id: int = m.chat.id @@ -457,7 +457,7 @@ async def pause_cmd_handler(c: Client, m: Message) -> None: else: await m.reply("Already paused or not playing.") -@Tele.on_message(filters.command('resume') & filters.me) +@Tele.on_message(filters.command('resume'), sudo=True) async def resume_cmd_handler(c: Client, m: Message) -> None: if not m.chat or m.chat.id is None or not c.me: return chat_id: int = m.chat.id @@ -467,7 +467,7 @@ async def resume_cmd_handler(c: Client, m: Message) -> None: else: await m.reply("Already playing or not playing.") -@Tele.on_message(filters.command('queue') & filters.me) +@Tele.on_message(filters.command('queue'), sudo=True) async def queue_cmd_handler(c: Client, m: Message) -> None: if not m.chat or m.chat.id is None or not c.me: return chat_id: int = m.chat.id @@ -495,7 +495,7 @@ async def queue_cmd_handler(c: Client, m: Message) -> None: await m.reply(res) -@Tele.on_message(filters.command('loop') & filters.me) +@Tele.on_message(filters.command('loop'), sudo=True) async def loop_cmd_handler(c: Client, m: Message) -> None: if not m.chat or not m.command or m.chat.id is None or not c.me: return diff --git a/Mods/ping.py b/Mods/ping.py index 1c54253..b756b9e 100644 --- a/Mods/ping.py +++ b/Mods/ping.py @@ -2,79 +2,126 @@ from pyrogram import filters from pyrogram.client import Client from pyrogram.types import ( - Message, - InlineKeyboardMarkup, - InlineKeyboardButton, + Message, InlineQuery, InlineQueryResultArticle, InputTextMessageContent ) import time -from datetime import datetime +import logging + +logger = logging.getLogger("Hazel.Ping") def get_readable_time(seconds: int) -> str: count = 0 + ping_time = "" time_list = [] - time_suffix_list = ["s", "m", "h", "d", "w"] # Adding week support for the requested format + time_suffix_list = ["s", "m", "h", "days"] - # Years - # ... ignoring years for now as boot time is unlikely to be that long + while count < 4: + count += 1 + if count < 3: + remainder, result = divmod(seconds, 60) + else: + remainder, result = divmod(seconds, 24) + if seconds == 0 and remainder == 0: + break + time_list.append(int(result)) + seconds = int(remainder) - # Weeks - weeks, seconds = divmod(seconds, 604800) - # Days - days, seconds = divmod(seconds, 86400) - # Hours - hours, seconds = divmod(seconds, 3600) - # Minutes - minutes, seconds = divmod(seconds, 60) - - parts = [] - if weeks: parts.append(f"{int(weeks)}w") - if days: parts.append(f"{int(days)}d") - if hours: parts.append(f"{int(hours)}h") - if minutes: parts.append(f"{int(minutes)}m") - parts.append(f"{int(seconds)}s") - - return ":".join(parts) + for i in range(len(time_list)): + time_list[i] = str(time_list[i]) + time_suffix_list[i] + if len(time_list) == 4: + ping_time += time_list.pop() + ", " -@Tele.on_message(filters.command("ping") & filters.me) + time_list.reverse() + ping_time += ":".join(time_list) + + return ping_time + +@Tele.on_message(filters.command("ping"), sudo=True) async def ping_cmd(c: Client, m: Message): - bot_username = (await Tele.bot.get_me()).username - results = await c.get_inline_bot_results(bot_username, "ping") - if results.results: - await c.send_inline_bot_result( - m.chat.id, - results.query_id, - results.results[0].id + start = time.time() + bot_me = await Tele.bot.get_me() + bot_username = bot_me.username + + # Calculate latency + end = time.time() + latency = int((end - start) * 1000) + uptime = get_readable_time(int(time.time() - START_TIME)) + + try: + results = await c.get_inline_bot_results(bot_username, f"ping {latency} {uptime}") + if results.results: + await c.send_inline_bot_result( + m.chat.id, + results.query_id, + results.results[0].id + ) + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass + else: + raise ValueError("No results") + except Exception as e: + logger.error(f"Inline ping failed: {e}") + await m.reply( + f"**Pong !!**\n" + f"**Latency -** `{latency}ms`\n" + f"**Uptime -** `{uptime}`" ) - await m.delete() -@Tele.on_message(filters.command("uptime") & filters.me) +@Tele.on_message(filters.command("uptime"), sudo=True) async def uptime_cmd(c: Client, m: Message): uptime = get_readable_time(int(time.time() - START_TIME)) - await m.edit(f"**Uptime -** `{uptime}`") + bot_me = await Tele.bot.get_me() + bot_username = bot_me.username + + try: + results = await c.get_inline_bot_results(bot_username, f"uptime {uptime}") + if results.results: + await c.send_inline_bot_result( + m.chat.id, + results.query_id, + results.results[0].id + ) + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass + else: + raise ValueError("No results") + except Exception as e: + logger.error(f"Inline uptime failed: {e}") + await m.reply(f"**Uptime -** `{uptime}`") # --- Inline Handlers --- -@Tele.bot.on_inline_query(filters.regex("^ping$")) +@Tele.bot.on_inline_query(filters.regex(r"^ping (\d+) (.*)")) async def ping_inline(c: Client, q: InlineQuery): - start = time.time() - # We use a dummy wait or just calculate current latency to show something - uptime = get_readable_time(int(time.time() - START_TIME)) - end = time.time() - ms = round((end - start) * 1000, 2) - + latency = q.matches[0].group(1) + uptime = q.matches[0].group(2) await q.answer([ InlineQueryResultArticle( title="Ping", - description="Check bot status.", + description=f"Latency: {latency}ms | Uptime: {uptime}", input_message_content=InputTextMessageContent( - f"**Pong !!** `{ms}ms`\n" + f"**Pong !!**\n" + f"**Latency -** `{latency}ms`\n" f"**Uptime -** `{uptime}`" ) ) ], cache_time=0) +@Tele.bot.on_inline_query(filters.regex(r"^uptime (.*)")) +async def uptime_inline(c: Client, q: InlineQuery): + uptime = q.matches[0].group(1) + await q.answer([ + InlineQueryResultArticle( + title="Uptime", + description=f"Uptime: {uptime}", + input_message_content=InputTextMessageContent(f"**Uptime -** `{uptime}`") + ) + ], cache_time=0) + MOD_NAME = "Ping" -MOD_HELP = "Check bot status.\n\nUsage:\n> .ping - Show Pong and Uptime in the requested format." +MOD_HELP = "**Usage:**\n> .ping - Check bot latency.\n> .uptime - Check bot uptime." diff --git a/Mods/purge.py b/Mods/purge.py index bb4c8c2..fb0ae16 100644 --- a/Mods/purge.py +++ b/Mods/purge.py @@ -7,21 +7,27 @@ logger = logging.getLogger("Hazel.Mods.purge") -@Tele.on_message(filters.command("purge") & filters.me & filters.group) +@Tele.on_message(filters.command("purge") & filters.group, fsudo=True) async def purgeFunc(app: Client, m: Message): + if m.from_user and m.from_user.id == app.me.id: + try: await m.delete() + except: pass + if not m.reply_to_message: return await m.reply("Reply to the message you want to delete from.") + start = m.reply_to_message.id end = m.id count = 0 - await m.edit("...") + for x in range(start, end + 1, 100): try: - x = list(range(x, x+101)) - count += await app.delete_messages(m.chat.id, x, revoke=True) # type: ignore + x_range = list(range(x, x+101)) + count += await app.delete_messages(m.chat.id, x_range, revoke=True) # type: ignore await asyncio.sleep(2.5) except Exception as e: logger.error(f"Error deleting messages {x}: {str(e)}") + await app.send_message(m.chat.id, f'Deleted {count} messages.') # type: ignore MOD_NAME = "Purge" diff --git a/Mods/repeater.py b/Mods/repeater.py index 49f5b4d..4d74628 100644 --- a/Mods/repeater.py +++ b/Mods/repeater.py @@ -6,8 +6,11 @@ # ---------------- Repeat ---------------- -@Tele.on_message(filters.command('repeat') & filters.me) +@Tele.on_message(filters.command('repeat'), sudo=True) async def repeatFunc(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass if 'help' in str(m.text): return await m.reply(MOD_HELP) text = m.text.split() # type: ignore @@ -53,8 +56,11 @@ async def repeatFunc(c: Client, m: Message): # ---------------- Groups ---------------- -@Tele.on_message(filters.command('rgroup') & filters.me) +@Tele.on_message(filters.command('rgroup'), sudo=True) async def groupCreate(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass text = m.text.split(maxsplit=2) # type: ignore if len(text) < 3 or text[1] != "create": @@ -73,8 +79,11 @@ async def groupCreate(c: Client, m: Message): return await m.reply(f"Group created: `{group['name']}` (id: {group['_id']})") -@Tele.on_message(filters.command('rgroup_add') & filters.me) +@Tele.on_message(filters.command('rgroup_add'), sudo=True) async def groupAdd(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass text = m.text.split(maxsplit=1) # type: ignore if len(text) < 2: @@ -104,8 +113,11 @@ async def groupAdd(c: Client, m: Message): return await m.reply(f"Chat added to group `{group['name']}`.") -@Tele.on_message(filters.command('rgroup_remove') & filters.me) +@Tele.on_message(filters.command('rgroup_remove'), sudo=True) async def groupRemove(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass text = m.text.split(maxsplit=1) # type: ignore if len(text) < 2: @@ -130,8 +142,11 @@ async def groupRemove(c: Client, m: Message): return await m.reply(f"Chat removed from group `{group['name']}`.") -@Tele.on_message(filters.command('rgroup_list') & filters.me) +@Tele.on_message(filters.command('rgroup_list'), sudo=True) async def groupList(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass text = m.text.split(maxsplit=1) # type: ignore if len(text) < 2: @@ -159,8 +174,11 @@ async def groupList(c: Client, m: Message): msg += "\n".join(f"`{x}`" for x in chats) return await m.reply(msg) -@Tele.on_message(filters.command('rgroup_list_all') & filters.me) +@Tele.on_message(filters.command('rgroup_list_all'), sudo=True) async def groupListAll(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass groups = await SQLClient.get_groups( c.me.id # type: ignore ) @@ -178,8 +196,11 @@ async def groupListAll(c: Client, m: Message): # ---------------- Repeat Management ---------------- -@Tele.on_message(filters.command('repeat_delete') & filters.me) +@Tele.on_message(filters.command('repeat_delete'), sudo=True) async def repeatDelete(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass text = m.text.split() # type: ignore if len(text) < 2: @@ -193,8 +214,11 @@ async def repeatDelete(c: Client, m: Message): return await m.reply(f"Error: {e}") -@Tele.on_message(filters.command('repeat_list') & filters.me) +@Tele.on_message(filters.command('repeat_list'), sudo=True) async def repeatList(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass rows = await SQLClient.get_repeat_messages() rows = [r for r in rows if r.get("userId") == c.me.id] # type: ignore @@ -213,8 +237,11 @@ async def repeatList(c: Client, m: Message): return await m.reply(msg) -@Tele.on_message(filters.command(['rpause', 'rresume']) & filters.me) +@Tele.on_message(filters.command(['rpause', 'rresume']), sudo=True) async def pauseAndResumeFunc(c: Client, m: Message): + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass import Hazel.Tasks.messageRepeater as messageRepeater uid = c.me.id # type: ignore if uid not in messageRepeater.events: diff --git a/Mods/sudo.py b/Mods/sudo.py new file mode 100644 index 0000000..2f975c3 --- /dev/null +++ b/Mods/sudo.py @@ -0,0 +1,76 @@ +from Hazel import Tele, SQLClient +from pyrogram import filters +from pyrogram.types import Message +from pyrogram.client import Client +import logging + +logger = logging.getLogger("Mods.Sudo") + +# Helper to get user ID from command +async def get_user_id(c: Client, m: Message) -> tuple: + if m.reply_to_message: + return m.reply_to_message.from_user.id, m.reply_to_message.from_user.first_name + + if len(m.command) < 2: + return None, None + + user_input = m.command[1] + try: + if user_input.isdigit(): + user_id = int(user_input) + user = await c.get_users(user_id) + return user.id, user.first_name + else: + user = await c.get_users(user_input.replace("@", "")) + return user.id, user.first_name + except Exception as e: + logger.error(f"Failed to get user: {e}") + return None, None + +@Tele.on_message(filters.command("sudo") & filters.me) +async def sudo_add(c: Client, m: Message): + user_id, name = await get_user_id(c, m) + if not user_id: + return await m.edit("❌ **Usage:** `.sudo ` or reply to a message.") + + await SQLClient.add_sudo(user_id, "sudo") + await m.edit(f"✅ **{name}** (`{user_id}`) has been added as **Sudo**.") + +@Tele.on_message(filters.command("fsudo") & filters.me) +async def fsudo_add(c: Client, m: Message): + user_id, name = await get_user_id(c, m) + if not user_id: + return await m.edit("❌ **Usage:** `.fsudo ` or reply to a message.") + + await SQLClient.add_sudo(user_id, "fsudo") + await m.edit(f"✅ **{name}** (`{user_id}`) has been added as **FSudo** (Full Sudo).") + +@Tele.on_message(filters.command(["rmvsudo", "unfsudo"]) & filters.me) +async def sudo_rm(c: Client, m: Message): + user_id, name = await get_user_id(c, m) + if not user_id: + return await m.edit("❌ **Usage:** `.rmvsudo ` or reply to a message.") + + await SQLClient.remove_sudo(user_id) + await m.edit(f"🗑️ Authorized access removed for **{name}** (`{user_id}`).") + +@Tele.on_message(filters.command("sudolist") & filters.me) +async def sudo_list(c: Client, m: Message): + all_users = await SQLClient.get_all_sudo() + if not all_users: + return await m.edit("⚠️ No sudo users found.") + + text = "**Authorized Sudo Users:**\n\n" + for u in all_users: + level = "**FSudo**" if u["level"] == "fsudo" else "Sudo" + text += f"• `{u['user_id']}` - {level}\n" + + await m.edit(text) + +@Tele.on_message(filters.command("reloadsudo") & filters.me) +async def sudo_reload(c: Client, m: Message): + await SQLClient.reload_sudo_cache() + await m.edit("✅ Sudo cache reloaded successfully.") + +MOD_NAME = "Auth" +MOD_HELP = "Manage authorized sudo users.\n\nUsage:\n> .sudo (reply/user) - Restricted sudo\n> .fsudo (reply/user) - Full sudo\n> .rmvsudo (reply/user) - Remove access\n> .sudolist - List all\n> .reloadsudo - Refresh cache" diff --git a/Mods/web-tools.py b/Mods/web-tools.py index 848259c..5f7ebb6 100644 --- a/Mods/web-tools.py +++ b/Mods/web-tools.py @@ -5,11 +5,8 @@ import webbrowser import asyncio -@Tele.on_message(filters.command('open') & filters.me) +@Tele.on_message(filters.command('open'), sudo=True) async def openCommand(client, m): - if Tele.getClientPrivilege(client) != 'sudo': - return await m.reply("You don't have permission.") - link = m.text.split(None, 1) if len(link) == 1: return await m.reply('Provide a link to open.') diff --git a/MultiSessionManagement/decorators.py b/MultiSessionManagement/decorators.py index 704e218..10a5d3f 100644 --- a/MultiSessionManagement/decorators.py +++ b/MultiSessionManagement/decorators.py @@ -1,4 +1,10 @@ from typing import TYPE_CHECKING, List, Tuple, Any +import logging +import pyrogram +from pyrogram import handlers, StopPropagation, ContinuePropagation +from pyrogram.types import Message + +logger = logging.getLogger("Hazel.Decorators") if TYPE_CHECKING: from MultiSessionManagement.telegram import Telegram @@ -6,34 +12,87 @@ Telegram = None class Decorators: - def on_message(self: Telegram, filters=None, group=0): # type: ignore + def on_message(self: Telegram, filters=None, group=0, sudo=False, fsudo=False, allow_all=False): # type: ignore def decorator(func): - # Ensure instance-level list exists + # Internal auth wrapper + async def auth_check(client, message: Message): + try: + # Extract ID + uid = None + if message.from_user: + uid = message.from_user.id + elif message.sender_chat: + uid = message.sender_chat.id + + if uid is None: + return + + # 1. Allow all (public listeners) + if allow_all: + return await func(client, message) + + # 2. Always allow the bot owner (me) + if uid == client.me.id: + return await func(client, message) + + from Hazel import SQLClient + if not SQLClient: + return + + # 3. Check for FSudo (Full access) + is_fsudo_user = await SQLClient.is_fsudo(uid) + + if fsudo: # Only owner or FSudo + if is_fsudo_user: + return await func(client, message) + return + + # 4. Check for Sudo (Restricted access) + if sudo: # Owner or FSudo or Sudo + is_sudo_user = await SQLClient.is_sudo(uid) + if is_fsudo_user or is_sudo_user: + return await func(client, message) + return + + # Default: reject others + return + # Do NOT catch propagation control exceptions + except (StopPropagation, ContinuePropagation): + raise + except Exception as e: + # Silent in production unless critical + pass + if not hasattr(self, "_message_handlers"): self._message_handlers = [] - # Store for later - self._message_handlers.append((filters, group, func)) + # Store for dynamic client loading + self._message_handlers.append((filters, group, auth_check)) - # Register to current clients - if hasattr(self, "_allClients"): - for i in self._allClients: - i.on_message(filters, group=group)(func) - return func + clients = getattr(self, "_allClients", []) + for client in clients: + try: + client.add_handler( + handlers.MessageHandler(auth_check, filters), + group + ) + except Exception: + pass + return auth_check return decorator def on_update(self: Telegram, *args): # type: ignore def decorator(func): - # Ensure instance-level list exists if not hasattr(self, "_update_handlers"): self._update_handlers = [] - # Store for later self._update_handlers.append((args, func)) - # Register to current clients - if hasattr(self, "_allPyTgCalls"): - for i in self._allPyTgCalls: - i.on_update(*args)(func) + calls = getattr(self, "_allPyTgCalls", []) + for pytgcalls in calls: + try: + pytgcalls.on_update(*args)(func) + except Exception: + pass return func return decorator \ No newline at end of file diff --git a/MultiSessionManagement/telegram.py b/MultiSessionManagement/telegram.py index 8131877..7937cec 100644 --- a/MultiSessionManagement/telegram.py +++ b/MultiSessionManagement/telegram.py @@ -3,13 +3,17 @@ from pytgcalls import PyTgCalls import pyrogram.filters as filters from pyrogram.types import Message, ChatPrivileges -from functools import partial -from typing import List, Dict, Optional, Protocol +from typing import List, Dict, Optional from pyrogram.enums import ChatMemberStatus from .TelegramMethods import Methods import logging +import pyrogram -logger = logging.getLogger("Telegram") +logger = logging.getLogger("Hazel.Telegram") + +# --- Global Prefix Patching --- +if not hasattr(filters, "_original_command"): + filters._original_command = filters.command class Telegram(Methods, Decorators): def __init__(self, config: tuple) -> None: @@ -19,7 +23,17 @@ def __init__(self, config: tuple) -> None: self.api_id: int = int(config[1]) self.api_hash: str = config[2] self.bot_token: str = config[0] - self.prefixes: List[str] = config[7] + + # Robust prefix parsing + raw_prefixes = config[7] + if isinstance(raw_prefixes, str): + self.prefixes = raw_prefixes.split() + else: + self.prefixes = list(raw_prefixes) + + if not self.prefixes: + self.prefixes = [".", "~", "$", "^"] + # ----------- Clients ------------ self.bot: Client = Client("HazelUB-Bot") self.mainClient: Client = Client("HazelUB") @@ -30,22 +44,20 @@ def __init__(self, config: tuple) -> None: self._clientPrivileges: Dict[Client, str] = {} self._clientPyTgCalls: Dict[Client, PyTgCalls] = {} - # Initializing handler lists on the instance + # Instance-level handler tracking self._message_handlers = [] self._update_handlers = [] - # Safer filters.command override - self._patch_filters_command() + # Force the global patch + self._apply_global_patch() - def _patch_filters_command(self): - """Standardize prefixes for all command filters.""" - if not hasattr(filters, "_original_command"): - filters._original_command = filters.command - + def _apply_global_patch(self): + """Standardize prefixes for all command filters globally.""" + current_prefixes = self.prefixes + def custom_command(commands: str | List[str], prefixes: str | List[str] = None, case_sensitive: bool = False): - if prefixes is None: - prefixes = self.prefixes - return filters._original_command(commands, prefixes, case_sensitive) + p = prefixes if prefixes is not None else current_prefixes + return filters._original_command(commands, p, case_sensitive) filters.command = custom_command @@ -64,6 +76,7 @@ async def create_pyrogram_clients(self) -> None: api_hash=self.api_hash, api_id=self.api_id ) + # User Accounts --------------------------------- self.mainClient = Client( name="HazelUB", @@ -75,10 +88,8 @@ async def create_pyrogram_clients(self) -> None: self._clientPrivileges[self.mainClient] = "sudo" self._clientPyTgCalls[self.mainClient] = mainClientPyTgCalls - # Load standard other sessions from config/env + # Load extra sessions from config/db all_other_sessions = list(self.othersessions) - - # Load extra sessions from Database from Hazel import SQLClient if SQLClient: db_sessions = await SQLClient.get_all_sessions() @@ -86,7 +97,7 @@ async def create_pyrogram_clients(self) -> None: if s not in all_other_sessions: all_other_sessions.append(s) - for session in all_other_sessions: # Other clients + for session in all_other_sessions: client = Client( name=f"HazelUB-{session[:10]}", session_string=session, @@ -94,10 +105,8 @@ async def create_pyrogram_clients(self) -> None: api_hash=self.api_hash ) clientPyTgCalls = PyTgCalls(client) - self._clientPrivileges[client] = "user" self._clientPyTgCalls[client] = clientPyTgCalls - self._allPyTgCalls.append(clientPyTgCalls) self.otherClients.append(client) @@ -107,7 +116,6 @@ async def create_pyrogram_clients(self) -> None: async def add_and_start_client(self, session_string: str) -> bool: """Dynamically add and start a new userbot client.""" try: - # Check if already exists for c in self._allClients: if c.session_string == session_string: return False @@ -119,9 +127,9 @@ async def add_and_start_client(self, session_string: str) -> bool: api_hash=self.api_hash ) - # Apply stored handlers + # Re-register ALL message handlers for f, group, func in self._message_handlers: - client.on_message(f, group=group)(func) + client.add_handler(pyrogram.handlers.MessageHandler(func, f), group) await client.start() @@ -130,22 +138,19 @@ async def add_and_start_client(self, session_string: str) -> bool: clientPyTgCalls = PyTgCalls(client) self._clientPyTgCalls[client] = clientPyTgCalls + # Re-register ALL update handlers for args, func in self._update_handlers: clientPyTgCalls.on_update(*args)(func) await clientPyTgCalls.start() - # Update lists self.otherClients.append(client) self._allClients.append(client) self._allPyTgCalls.append(clientPyTgCalls) - # Join channel from Hazel import __channel__ - try: - await client.join_chat(__channel__) - except: - pass + try: await client.join_chat(__channel__) + except: pass return True except Exception as e: @@ -153,15 +158,12 @@ async def add_and_start_client(self, session_string: str) -> bool: return False async def start(self) -> None: - # HazelUB await self.bot.start() - for client in self._allClients: try: await client.start() pytgcalls = self.getClientPyTgCalls(client) - if pytgcalls: - await pytgcalls.start() + if pytgcalls: await pytgcalls.start() except Exception as e: logger.error(f"Failed to start client {client.name}: {e}") @@ -170,15 +172,12 @@ async def start(self) -> None: for client in self._allClients: if client.is_connected: await client.join_chat(__channel__) - except: - pass + except: pass async def stop(self) -> None: for client in self._allClients: - if client.is_connected: - await client.stop() - if self.bot.is_connected: - await self.bot.stop() + if client.is_connected: await client.stop() + if self.bot.is_connected: await self.bot.stop() def getClientById(self, id: int | None = 0, m: Message | None = None) -> Optional[Client]: if m and isinstance(m, Message): @@ -197,21 +196,16 @@ def getClientPyTgCalls(self, client: Client) -> Optional[PyTgCalls]: async def is_admin(self, client: Client, chat_id: int, user_id: Optional[int] = None) -> bool: try: - if not user_id: - user_id = client.me.id # type: ignore + if not user_id: user_id = client.me.id # type: ignore member = await client.get_chat_member(chat_id, user_id) - return member.status in ( - ChatMemberStatus.ADMINISTRATOR, - ChatMemberStatus.OWNER, - ) + return member.status in (ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER) except Exception as e: logger.error(f"Error checking admin status: {str(e)}") return False async def get_chat_member_privileges(self, client: Client, chat_id: int, user_id: Optional[int] = None) -> Optional[ChatPrivileges]: try: - if not user_id: - user_id = client.me.id # type: ignore + if not user_id: user_id = client.me.id # type: ignore member = await client.get_chat_member(chat_id, user_id) return member.privileges except Exception as e: diff --git a/Setup/main.py b/Setup/main.py index 560f84c..df1adce 100644 --- a/Setup/main.py +++ b/Setup/main.py @@ -31,6 +31,9 @@ async def main(install_packages: bool=True): logger.info("Loading Mods...") import Mods; Mods.load_mods() + if Hazel.SQLClient: + await Hazel.SQLClient.reload_sudo_cache() + logger.info("HazelUB is now running!") await asyncio.to_thread(startup_popup) except Exception as e: diff --git a/Setup/utils.py b/Setup/utils.py index e82d41a..4afa9b9 100644 --- a/Setup/utils.py +++ b/Setup/utils.py @@ -48,10 +48,18 @@ def load_config() -> tuple: MONGO_URL = getattr(config, 'MONGO_URL', None) or os.getenv('MONGO_URL') or _ask_missing("MONGO_URL") REDIS_URL = getattr(config, 'REDIS_URL', None) or os.getenv('REDIS_URL') # ---------- Optional ---------- - OtherSessions = getattr(config, 'OtherSessions', []) or list(os.getenv('OtherSessions', [])) - PREFIX = list(getattr(config, 'PREFIX', [])) or os.getenv('PREFIX', []) + OtherSessions = getattr(config, 'OtherSessions', []) + if not OtherSessions: + env_others = os.getenv('OtherSessions', "") + OtherSessions = env_others.split() if env_others else [] + + PREFIX = getattr(config, 'PREFIX', []) + if not PREFIX: + env_prefix = os.getenv('PREFIX', ". ~ $ ^") + PREFIX = env_prefix.split() if env_prefix else [".", "~", "$", "^"] + GEMINI_API_KEY = getattr(config, 'GEMINI_API_KEY', '') or os.getenv('GEMINI_API_KEY', '') - return (BOT_TOKEN, API_ID, API_HASH, SESSION, MONGO_URL, REDIS_URL, OtherSessions, PREFIX, GEMINI_API_KEY) + return (BOT_TOKEN, API_ID, API_HASH, SESSION, MONGO_URL, REDIS_URL, list(OtherSessions), list(PREFIX), GEMINI_API_KEY) def startup_popup(): from plyer import notification diff --git a/requirements.txt b/requirements.txt index 9f8a324..f6c77cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ plyer motor redis websockets -google-genai \ No newline at end of file +google-genai +tzdata \ No newline at end of file From 56eed72f858b33eea5992b849093ebfc8e3df996 Mon Sep 17 00:00:00 2001 From: Otazuki <115762083+Otazuki004@users.noreply.github.com> Date: Sat, 21 Feb 2026 17:51:37 +0530 Subject: [PATCH 15/16] Refactor ping command and remove inline handlers --- Mods/ping.py | 87 +++++----------------------------------------------- 1 file changed, 7 insertions(+), 80 deletions(-) diff --git a/Mods/ping.py b/Mods/ping.py index b756b9e..0329733 100644 --- a/Mods/ping.py +++ b/Mods/ping.py @@ -1,12 +1,7 @@ from Hazel import Tele, START_TIME from pyrogram import filters from pyrogram.client import Client -from pyrogram.types import ( - Message, - InlineQuery, - InlineQueryResultArticle, - InputTextMessageContent -) +from pyrogram.types import Message import time import logging @@ -45,83 +40,15 @@ async def ping_cmd(c: Client, m: Message): bot_me = await Tele.bot.get_me() bot_username = bot_me.username - # Calculate latency end = time.time() latency = int((end - start) * 1000) uptime = get_readable_time(int(time.time() - START_TIME)) - try: - results = await c.get_inline_bot_results(bot_username, f"ping {latency} {uptime}") - if results.results: - await c.send_inline_bot_result( - m.chat.id, - results.query_id, - results.results[0].id - ) - if m.from_user and m.from_user.id == c.me.id: - try: await m.delete() - except: pass - else: - raise ValueError("No results") - except Exception as e: - logger.error(f"Inline ping failed: {e}") - await m.reply( - f"**Pong !!**\n" - f"**Latency -** `{latency}ms`\n" - f"**Uptime -** `{uptime}`" - ) - -@Tele.on_message(filters.command("uptime"), sudo=True) -async def uptime_cmd(c: Client, m: Message): - uptime = get_readable_time(int(time.time() - START_TIME)) - bot_me = await Tele.bot.get_me() - bot_username = bot_me.username - - try: - results = await c.get_inline_bot_results(bot_username, f"uptime {uptime}") - if results.results: - await c.send_inline_bot_result( - m.chat.id, - results.query_id, - results.results[0].id - ) - if m.from_user and m.from_user.id == c.me.id: - try: await m.delete() - except: pass - else: - raise ValueError("No results") - except Exception as e: - logger.error(f"Inline uptime failed: {e}") - await m.reply(f"**Uptime -** `{uptime}`") - -# --- Inline Handlers --- - -@Tele.bot.on_inline_query(filters.regex(r"^ping (\d+) (.*)")) -async def ping_inline(c: Client, q: InlineQuery): - latency = q.matches[0].group(1) - uptime = q.matches[0].group(2) - await q.answer([ - InlineQueryResultArticle( - title="Ping", - description=f"Latency: {latency}ms | Uptime: {uptime}", - input_message_content=InputTextMessageContent( - f"**Pong !!**\n" - f"**Latency -** `{latency}ms`\n" - f"**Uptime -** `{uptime}`" - ) - ) - ], cache_time=0) - -@Tele.bot.on_inline_query(filters.regex(r"^uptime (.*)")) -async def uptime_inline(c: Client, q: InlineQuery): - uptime = q.matches[0].group(1) - await q.answer([ - InlineQueryResultArticle( - title="Uptime", - description=f"Uptime: {uptime}", - input_message_content=InputTextMessageContent(f"**Uptime -** `{uptime}`") - ) - ], cache_time=0) + await m.reply( + f"**Pong !!**\n" + f"**Latency -** `{latency}ms`\n" + f"**Uptime -** `{uptime}`" + ) MOD_NAME = "Ping" -MOD_HELP = "**Usage:**\n> .ping - Check bot latency.\n> .uptime - Check bot uptime." +MOD_HELP = "**Usage:**\n> .ping - Check bot latency $ uptime." From a4242227c11aa818721e028b2c0083634e2f45a1 Mon Sep 17 00:00:00 2001 From: Otazuki <115762083+Otazuki004@users.noreply.github.com> Date: Sat, 21 Feb 2026 18:06:59 +0530 Subject: [PATCH 16/16] revert --- Mods/ping.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 7 deletions(-) diff --git a/Mods/ping.py b/Mods/ping.py index 0329733..b756b9e 100644 --- a/Mods/ping.py +++ b/Mods/ping.py @@ -1,7 +1,12 @@ from Hazel import Tele, START_TIME from pyrogram import filters from pyrogram.client import Client -from pyrogram.types import Message +from pyrogram.types import ( + Message, + InlineQuery, + InlineQueryResultArticle, + InputTextMessageContent +) import time import logging @@ -40,15 +45,83 @@ async def ping_cmd(c: Client, m: Message): bot_me = await Tele.bot.get_me() bot_username = bot_me.username + # Calculate latency end = time.time() latency = int((end - start) * 1000) uptime = get_readable_time(int(time.time() - START_TIME)) - await m.reply( - f"**Pong !!**\n" - f"**Latency -** `{latency}ms`\n" - f"**Uptime -** `{uptime}`" - ) + try: + results = await c.get_inline_bot_results(bot_username, f"ping {latency} {uptime}") + if results.results: + await c.send_inline_bot_result( + m.chat.id, + results.query_id, + results.results[0].id + ) + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass + else: + raise ValueError("No results") + except Exception as e: + logger.error(f"Inline ping failed: {e}") + await m.reply( + f"**Pong !!**\n" + f"**Latency -** `{latency}ms`\n" + f"**Uptime -** `{uptime}`" + ) + +@Tele.on_message(filters.command("uptime"), sudo=True) +async def uptime_cmd(c: Client, m: Message): + uptime = get_readable_time(int(time.time() - START_TIME)) + bot_me = await Tele.bot.get_me() + bot_username = bot_me.username + + try: + results = await c.get_inline_bot_results(bot_username, f"uptime {uptime}") + if results.results: + await c.send_inline_bot_result( + m.chat.id, + results.query_id, + results.results[0].id + ) + if m.from_user and m.from_user.id == c.me.id: + try: await m.delete() + except: pass + else: + raise ValueError("No results") + except Exception as e: + logger.error(f"Inline uptime failed: {e}") + await m.reply(f"**Uptime -** `{uptime}`") + +# --- Inline Handlers --- + +@Tele.bot.on_inline_query(filters.regex(r"^ping (\d+) (.*)")) +async def ping_inline(c: Client, q: InlineQuery): + latency = q.matches[0].group(1) + uptime = q.matches[0].group(2) + await q.answer([ + InlineQueryResultArticle( + title="Ping", + description=f"Latency: {latency}ms | Uptime: {uptime}", + input_message_content=InputTextMessageContent( + f"**Pong !!**\n" + f"**Latency -** `{latency}ms`\n" + f"**Uptime -** `{uptime}`" + ) + ) + ], cache_time=0) + +@Tele.bot.on_inline_query(filters.regex(r"^uptime (.*)")) +async def uptime_inline(c: Client, q: InlineQuery): + uptime = q.matches[0].group(1) + await q.answer([ + InlineQueryResultArticle( + title="Uptime", + description=f"Uptime: {uptime}", + input_message_content=InputTextMessageContent(f"**Uptime -** `{uptime}`") + ) + ], cache_time=0) MOD_NAME = "Ping" -MOD_HELP = "**Usage:**\n> .ping - Check bot latency $ uptime." +MOD_HELP = "**Usage:**\n> .ping - Check bot latency.\n> .uptime - Check bot uptime."