From 50281cbec88b1e3653dbb0bcfe99f7ceaa717f27 Mon Sep 17 00:00:00 2001 From: illvart Date: Thu, 19 Mar 2026 04:00:35 +0700 Subject: [PATCH] update --- .github/workflows/ci.yml | 2 +- Dockerfile | 4 +- README.md | 2 +- ds/__init__.py | 6 +-- ds/__main__.py | 7 +++- ds/config.py | 46 ++++++++++---------- ds/kasta.py | 90 +++++++++++++++++++++------------------- ds/patcher.py | 17 +++----- ds/plugins/delayspam.py | 9 ++-- ds/plugins/misc.py | 23 +++++----- manifest.json | 2 +- pyproject.toml | 2 +- 12 files changed, 107 insertions(+), 103 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 988dd22..26f1b35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.13', '3.14'] + python-version: ['3.14'] steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 diff --git a/Dockerfile b/Dockerfile index 64e27d2..47c8752 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,8 @@ # https://github.com/kastaid/ds # MIT License -FROM python:3.13-slim-bookworm -COPY --from=ghcr.io/astral-sh/uv:0.10.10 /uv /uvx /bin/ +FROM python:3.14-slim-bookworm +COPY --from=ghcr.io/astral-sh/uv:0.10.11 /uv /uvx /bin/ ENV TERM=xterm \ PATH=/opt/venv/bin:$PATH \ UV_LINK_MODE=copy \ diff --git a/README.md b/README.md index 96f521c..737d16e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ ## Requirements -- Python 3.13+ +- Python 3.14+ - Linux (Debian/Ubuntu) - Telegram `API_ID` and `API_HASH` from [API development tools](https://my.telegram.org) diff --git a/ds/__init__.py b/ds/__init__.py index bec65a7..589610b 100644 --- a/ds/__init__.py +++ b/ds/__init__.py @@ -4,12 +4,12 @@ from pathlib import Path from shutil import rmtree -from time import time +from time import monotonic from version import __version__ # noqa PROJECT = "ds" -StartTime = time() +StartTime = monotonic() Root = Path(__file__).parent.parent WORKERS = 3 @@ -24,4 +24,4 @@ else: i.unlink(missing_ok=True) -del Path, rmtree, time +del Path, rmtree, monotonic diff --git a/ds/__main__.py b/ds/__main__.py index 9ec5c5c..a07fe98 100644 --- a/ds/__main__.py +++ b/ds/__main__.py @@ -5,7 +5,7 @@ import sys import uvloop -from pyrogram.sync import compose +from pyrogram import idle from .kasta import KastaClient from .logger import LOG @@ -13,7 +13,10 @@ async def main() -> None: - await compose([KastaClient()]) + app = KastaClient() + await app.start() + await idle() + await app.stop() if __name__ == "__main__": diff --git a/ds/config.py b/ds/config.py index c83c8db..b9da3a7 100644 --- a/ds/config.py +++ b/ds/config.py @@ -1,39 +1,39 @@ +# ruff: noqa: RUF012 # Copyright (C) 2023-present kastaid # https://github.com/kastaid/ds # MIT License from os import getenv -from typing import ClassVar -from dotenv import find_dotenv, load_dotenv +from dotenv import load_dotenv -from . import WORKERS +from . import WORKERS, Root -load_dotenv(find_dotenv()) +load_dotenv(Root / ".env", override=True) -def tobool(val: str) -> int | None: - """ - Convert a string representation of truth to true (1) or false (0). - https://github.com/python/cpython/blob/main/Lib/distutils/util.py - """ - val = val.lower() - if val in {"y", "yes", "t", "true", "on", "1"}: - return 1 - if val in {"n", "no", "f", "false", "off", "0"}: - return 0 - raise ValueError(f"invalid truth value {val!r}") +def env(key: str, default: str = "") -> str: + return getenv(key, default).strip() + + +def to_bool(value: str) -> bool: + value = value.lower() + if value in {"y", "yes", "t", "true", "on", "1", "enable", "enabled"}: + return True + if value in {"n", "no", "f", "false", "off", "0", "disable", "disabled"}: + return False + raise ValueError(f"Invalid boolean value: {value!r}") class Var: - DEV_MODE: bool = tobool(getenv("DEV_MODE", "false").strip()) - API_ID: int = int(getenv("API_ID", "0").strip()) - API_HASH: str = getenv("API_HASH", "").strip() - STRING_SESSION: str = getenv("STRING_SESSION", "").strip() - WORKERS: int = int(getenv("WORKERS", str(WORKERS)).strip()) - HANDLER: str = getenv("HANDLER", "").strip() + DEV_MODE: bool = to_bool(env("DEV_MODE", "false")) + API_ID: int = int(env("API_ID", "0")) + API_HASH: str = env("API_HASH", "") + STRING_SESSION: str = env("STRING_SESSION", "") + HANDLER: str = env("HANDLER", "") + WORKERS: int = int(env("WORKERS", str(WORKERS))) IS_STARTUP: bool = False - ERROR_RETRY: ClassVar[dict[int, int]] = {} + ERROR_RETRY: dict[int, int] = {} -del load_dotenv, find_dotenv, WORKERS +del load_dotenv, WORKERS, Root diff --git a/ds/kasta.py b/ds/kasta.py index 3d01b05..b75c0be 100644 --- a/ds/kasta.py +++ b/ds/kasta.py @@ -6,21 +6,29 @@ import random import sys from platform import machine, version -from time import time +from time import monotonic +from typing import TYPE_CHECKING from pyrogram.client import Client as RawClient from pyrogram.connection.transport import TCPAbridged from pyrogram.enums import ParseMode -from pyrogram.errors import RPCError -from pyrogram.types import CallbackQuery, User +from pyrogram.raw.all import layer from . import PROJECT, Root, StartTime from .config import Var from .helpers import time_formatter +from .logger import LOG + +if TYPE_CHECKING: + from loguru._logger import Logger + from pyrogram.types import User class KastaClient(RawClient): + log: Logger = LOG + def __init__(self): + self._stopped: bool = False self._me: User | None = None super().__init__( PROJECT, @@ -44,38 +52,56 @@ async def get_me( self._me = await super().get_me() return self._me - async def start(self) -> None: + async def start(self) -> KastaClient: try: - self.log.info("Starting Userbot Client...") - delay = random.uniform(3.5, 6.5) if Var.DEV_MODE else random.uniform(1.5, 3.5) - await asyncio.sleep(delay) + if not Var.API_ID: + raise ValueError("Required: API_ID not set in .env") + if not Var.API_HASH: + raise ValueError("Required: API_HASH not set in .env") + if not Var.STRING_SESSION: + raise ValueError("Required: STRING_SESSION not set in .env") + self.log.info(">> 🚀 STARTING USERBOT...") + _jitter = (3.5, 6.5) if Var.DEV_MODE else (1.5, 3.5) + await asyncio.sleep(random.uniform(*_jitter)) await super().start() + self.me = await self.get_me() except Exception as err: self.log.exception(err) - self.log.error("Userbot Client exiting.") + self.log.error(">> USERBOT EXITING.") sys.exit(1) - self.me = await self.get_me() - user_details = "Userbot Client details:\n" - user_details += f"ID: {self.me.id}\n" - user_details += f"First Name: {self.me.first_name}" + _me = [ + ">> USERBOT DETAILS:", + f"ID: {self.me.id}", + f"First Name: {self.me.first_name}", + ] if self.me.last_name: - user_details += f"\nLast Name: {self.me.last_name}" + _me.append(f"Last Name: {self.me.last_name}") if self.me.username: - user_details += f"\nUsername: @{self.me.username}" - self.log.info(user_details) - await self.__follow_us() - done = time_formatter(time() - StartTime) - self.log.success(f">> 🔥 USERBOT RUNNING IN {done} !!") + _me.append(f"Username: @{self.me.username}") + if self.me.dc_id: + _me.append(f"DC: {self.me.dc_id}") + _me.append(f"Layer: {layer}") + self.log.info("\n".join(_me)) + await self.__join_us() + done = time_formatter(monotonic() - StartTime) + launch = f">> 🚀 Userbot launched in {done}, layer: {layer}." + await self.send_message("me", launch) + self.log.success(f">> 🔥 USERBOT UP IN {done}.") Var.IS_STARTUP = True + return self - async def stop(self, **_) -> None: + async def stop(self, block: bool = True) -> KastaClient: + if self._stopped: + return self + self._stopped = True try: - await super().stop() - self.log.info("Stopped Client.") + await super().stop(block=block) + self.log.warning(">> USERBOT STOPPED.") except BaseException: pass + return self - async def __follow_us(self) -> None: + async def __join_us(self) -> None: try: await self.join_chat(-1001174631272) await asyncio.sleep(random.uniform(3.5, 6.5)) @@ -85,23 +111,3 @@ async def __follow_us(self) -> None: await self.join_chat(-1001699144606) except BaseException: pass - - async def answer( - self, - callback: CallbackQuery, - **args, - ) -> None: - try: - await callback.answer(**args) - except RPCError: - pass - - async def try_delete(self, event) -> bool: - if not event: - return False - is_callback = isinstance(event, CallbackQuery) - message = event.message if is_callback else event - deleted = await message.delete() - if not deleted and is_callback: - await self.answer(event) - return deleted diff --git a/ds/patcher.py b/ds/patcher.py index 9008268..281a118 100644 --- a/ds/patcher.py +++ b/ds/patcher.py @@ -3,25 +3,22 @@ # MIT License import asyncio -import logging import random -from collections.abc import Callable -from functools import wraps -from typing import Any, T +from typing import TYPE_CHECKING, Any import pyrogram.client import pyrogram.errors import pyrogram.types.messages_and_media.message -from .logger import LOG +if TYPE_CHECKING: + from collections.abc import Callable -def patch(target: Any): +def patch[T](target: Any) -> Callable[[type[T]], type[T]]: def is_patchable(item: tuple[str, Any]) -> bool: return getattr(item[1], "patchable", False) - @wraps(target) - def wrapper(container: type[T]) -> T: + def wrapper(container: type[T]) -> type[T]: for name, func in filter(is_patchable, container.__dict__.items()): old = getattr(target, name, None) if old is not None: @@ -46,10 +43,6 @@ def wrapper(func: Callable) -> Callable: @patch(pyrogram.client.Client) class Client: - @patchable(True) - def log(self) -> logging: - return LOG - @patchable() async def invoke(self, *args, **kwargs): try: diff --git a/ds/plugins/delayspam.py b/ds/plugins/delayspam.py index 04b85b9..fbec4d9 100644 --- a/ds/plugins/delayspam.py +++ b/ds/plugins/delayspam.py @@ -3,14 +3,18 @@ # MIT License import asyncio +import random +from typing import TYPE_CHECKING from pyrogram import errors, filters from pyrogram.enums import ParseMode -from pyrogram.types import Message from ds.config import Var from ds.kasta import KastaClient +if TYPE_CHECKING: + from pyrogram.types import Message + DS_TASKS: dict[int, dict[int, asyncio.Task]] = {i: {} for i in range(10)} @@ -152,7 +156,7 @@ async def run_delayspam( if chat_id not in get_task_store(ds): break try: - await asyncio.sleep(1) + await asyncio.sleep(random.uniform(3.5, 6.5)) result = await send_message( client, message, @@ -166,7 +170,6 @@ async def run_delayspam( except errors.RPCError: pass except Exception as err: - client.log.error(err) client.log.exception(err) if chat_id not in Var.ERROR_RETRY: Var.ERROR_RETRY.update({chat_id: 1}) diff --git a/ds/plugins/misc.py b/ds/plugins/misc.py index b1a61f1..63691e0 100644 --- a/ds/plugins/misc.py +++ b/ds/plugins/misc.py @@ -3,7 +3,8 @@ # MIT License import asyncio -from time import monotonic, time +import random +from time import monotonic from pyrogram import filters from pyrogram.errors import RPCError, UsersTooMuch @@ -35,14 +36,12 @@ async def _ping(_, m): ) text = "🏓 Pong !!\n" text += f"Speed – {monotonic() - start:.3f}s\n" - text += f"Uptime – {time_formatter(time() - StartTime)}" + text += f"Uptime – {time_formatter(monotonic() - StartTime)}" try: + await m.delete() await msg.edit(text) except BaseException: - try: - await msg.delete() - except BaseException: - pass + await msg.delete() await m.reply( text, quote=False, @@ -86,7 +85,7 @@ async def _logs(_, m): caption=f"Terminal Logs {count}", quote=False, ) - await asyncio.sleep(1) + await asyncio.sleep(random.uniform(1.5, 3.5)) await msg.delete() @@ -156,7 +155,7 @@ async def _purge(c, m): except RPCError: pass chunk.clear() - await asyncio.sleep(1) + await asyncio.sleep(random.uniform(1.5, 3.5)) if len(chunk) > 0: try: await c.delete_messages(chat_id, chunk) @@ -224,14 +223,14 @@ async def _join(c, m): state = bool(await c.join_chat(chat_id)) except UsersTooMuch: count += 1 - await m.edit(rf"🔃 Join retry {count}...") - await asyncio.sleep(6) + await m.edit(f"🔃 Join retry {count}...") + await asyncio.sleep(random.uniform(6.5, 8.5)) continue except BaseException: break if state: break - text = rf"✅ Joined as {count}." if state else r"❌ Error" + text = f"✅ Joined as {count}." if state else "❌ Error" await m.edit(text) @@ -259,7 +258,7 @@ async def _leave(c, m): state = bool(await c.leave_chat(chat_id, delete=True)) except BaseException: pass - text = r"✅ Leaved" if state else r"❌ Error" + text = "✅ Leaved" if state else "❌ Error" await m.edit(text) diff --git a/manifest.json b/manifest.json index c1a1b88..58f0b7c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,3 +1,3 @@ { - "version": "0.3.5" + "version": "0.3.6" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 55d39ad..5d3f56f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ show-fixes = true line-length = 120 indent-width = 4 -target-version = "py313" +target-version = "py314" [tool.ruff.format] quote-style = "double"