From b031526537d729339566b5de903ae2e70dd8b55e Mon Sep 17 00:00:00 2001 From: illvart Date: Tue, 23 Jan 2024 02:48:12 +0700 Subject: [PATCH] update --- .dockerignore | 2 + .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/bug_report.md | 27 ++ .github/ISSUE_TEMPLATE/feature_request.md | 14 ++ .github/stale.yml | 18 ++ .github/workflows/ci.yml | 43 ++++ Dockerfile | 11 +- README.md | 9 +- ds/__init__.py | 15 +- ds/__main__.py | 30 +-- ds/bot.py | 90 ------- ds/config.py | 8 +- ds/helpers.py | 27 +- ds/logger.py | 16 +- ds/patcher.py | 52 ++++ ds/plugins/delayspam.py | 290 +++++++--------------- ds/plugins/misc.py | 106 ++++++-- ds/user.py | 92 +++++++ manifest.json | 2 +- requirements-dev.txt | 8 +- requirements.txt | 4 +- setup.cfg | 6 +- 22 files changed, 492 insertions(+), 380 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/stale.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 ds/bot.py create mode 100644 ds/patcher.py create mode 100644 ds/user.py diff --git a/.dockerignore b/.dockerignore index 29a5a46..da2fd2e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,3 +16,5 @@ requirements-dev.txt run.py scripts setup.cfg +manifest.json +version.py diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c4d61c9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [illvart] +custom: ["https://linktr.ee/illvart"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..5416f75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Python Version** +Provide exact python version used + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..81e6b06 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..9d8075f --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,18 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 30 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - accepted +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed in 7 days if no further activity occurs. + To prevent this from happening, leave a comment. +# Comment to post when closing a stale Issue or Pull Request. +closeComment: > + Closing this issue because it has been marked as stale for more than 7 days. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4a6664f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI +on: [push, pull_request, workflow_dispatch] +jobs: + linter: + name: Run linting and format code + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: styfle/cancel-workflow-action@0.12.0 + with: + all_but_latest: true + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v4 + if: startsWith(runner.os, 'Linux') + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements-dev.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Install dependencies + run: | + python3 -m pip install -U pip + pip3 install -r requirements.txt + if [ -f requirements-dev.txt ]; then pip3 install -r requirements-dev.txt; fi + - name: Linting and format code + run: python3 -m run --lint + - uses: illvart/beautysh-action@latest + with: + args: "*.sh --indent-size 2 &>/dev/null" + - uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "[action] ci: auto-fixes" + commit_options: "--no-verify" + commit_user_name: kastaid + commit_user_email: illvart@protonmail.com + commit_author: kastaid \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 13fdea7..13be5af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,10 @@ -FROM python:3.10-slim-bullseye +FROM python:3.11-slim-bookworm ENV TZ=Asia/Jakarta \ TERM=xterm-256color \ DEBIAN_FRONTEND=noninteractive \ - PIP_NO_CACHE_DIR=1 \ - VIRTUAL_ENV=/venv \ - PATH=/venv/bin:/app/bin:$PATH + VIRTUAL_ENV=/opt/venv \ + PATH=/opt/venv/bin:/app/bin:$PATH WORKDIR /app COPY . . @@ -17,8 +16,8 @@ RUN set -ex \ && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone \ && dpkg-reconfigure --force -f noninteractive tzdata >/dev/null 2>&1 \ && python3 -m venv $VIRTUAL_ENV \ - && pip3 install --disable-pip-version-check --default-timeout=100 --no-cache-dir -r requirements.txt \ + && pip3 install --disable-pip-version-check --default-timeout=100 -r requirements.txt \ && apt-get -qqy clean \ - && rm -rf -- ~/.cache /var/lib/apt/lists/* /var/cache/apt/archives/* /etc/apt/sources.list.d/* /usr/share/man/* /usr/share/doc/* /var/log/* /tmp/* /var/tmp/* + && rm -rf -- /var/lib/apt/lists/* /var/cache/apt/archives/* /etc/apt/sources.list.d/* /usr/share/man/* /usr/share/doc/* /var/log/* /tmp/* /var/tmp/* CMD ["python3", "-m", "ds"] diff --git a/README.md b/README.md index 8a6396c..bc22812 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ > Pyrogram **userbot** for delay spam a message with multi chats.

+ CI Version LICENSE Telegram @@ -26,7 +27,7 @@ HANDLER= ```sh git pull \ && docker system prune -f \ - && docker compose up --detach --build --remove-orphans --no-color \ + && docker compose up --detach --build --remove-orphans \ && docker compose logs -f ``` @@ -34,7 +35,13 @@ git pull \ Please read how to at [delayspam.py](https://github.com/kastaid/ds/blob/main/ds/plugins/delayspam.py). ```sh ds 5 10 ok +ds1 9 5 cool + dscancel +ds1cancel + dsstop +ds1stop + dsclear ``` diff --git a/ds/__init__.py b/ds/__init__.py index 39a6008..8b02822 100644 --- a/ds/__init__.py +++ b/ds/__init__.py @@ -5,30 +5,25 @@ # Please read the MIT License in # < https://github.com/kastaid/ds/blob/main/LICENSE/ >. -from asyncio import set_event_loop -from multiprocessing import cpu_count +from os import cpu_count from pathlib import Path from shutil import rmtree from time import time -import uvloop from version import __version__ PROJECT = "ds" StartTime = time() Root: Path = Path(__file__).parent.parent -LOOP = uvloop.new_event_loop() -set_event_loop(LOOP) -WORKERS = cpu_count() * 5 +WORKERS = min(32, (cpu_count() or 1) + 4) -DIRS = ("logs/",) -for d in DIRS: +for d in ("logs/",): if not (Root / d).exists(): (Root / d).mkdir(parents=True, exist_ok=True) else: for _ in (Root / d).rglob("*"): if _.is_dir(): - rmtree(_) + rmtree(_, ignore_errors=True) else: _.unlink(missing_ok=True) -del set_event_loop, uvloop, Path, cpu_count, rmtree, time +del cpu_count, Path, rmtree, time diff --git a/ds/__main__.py b/ds/__main__.py index c2b94b5..734e35c 100644 --- a/ds/__main__.py +++ b/ds/__main__.py @@ -5,39 +5,33 @@ # Please read the MIT License in # < https://github.com/kastaid/ds/blob/main/LICENSE/ >. -import asyncio import sys -from pyrogram.raw.functions.account import DeleteAccount +import uvloop from pyrogram.sync import idle -from . import LOOP -from .bot import User -from .logger import LOGS - -DeleteAccount.__new__ = None +from ds.logger import LOG +from ds.patcher import * # noqa +from ds.user import UserClient async def main() -> None: - await User.start() + await UserClient().start() await idle() - await User.stop() + await UserClient().stop() if __name__ == "__main__": try: - LOOP.run_until_complete(main()) + uvloop.run(main()) except ( - ConnectionError, - TimeoutError, - asyncio.exceptions.CancelledError, + KeyboardInterrupt, + SystemExit, ): pass - except RuntimeError as err: - LOGS.warning(f"[MAIN_WARNING] : {err}") except ImportError as err: - LOGS.exception(f"[MAIN_MODULE_IMPORT] : {err}") + LOG.exception(f"[MAIN_MODULE_IMPORT] : {err}") sys.exit(1) except Exception as err: - LOGS.exception(f"[MAIN_ERROR] : {err}") + LOG.exception(f"[MAIN_ERROR] : {err}") finally: - LOGS.warning("[MAIN] - Stopped...") + LOG.warning("[MAIN] - Stopped...") sys.exit(0) diff --git a/ds/bot.py b/ds/bot.py deleted file mode 100644 index e7ed2f2..0000000 --- a/ds/bot.py +++ /dev/null @@ -1,90 +0,0 @@ -# ds < https://t.me/kastaid > -# Copyright (C) 2023-present kastaid -# -# This file is a part of < https://github.com/kastaid/ds/ > -# Please read the MIT License in -# < https://github.com/kastaid/ds/blob/main/LICENSE/ >. - -import os -import subprocess -import sys -from asyncio import sleep -from sqlite3 import OperationalError -from time import time -from typing import Any -from pyrogram.client import Client as RawClient -from pyrogram.enums.parse_mode import ParseMode -from pyrogram.errors import FloodWait, NotAcceptable, Unauthorized -from pyrogram.types import User -from . import PROJECT, StartTime, Root -from .config import Var -from .helpers import time_formatter, restart -from .logger import LOGS - - -class Client(RawClient): - def __init__(self, **kwargs: Any): - self._me: User | None = None - self._is_bot: bool = bool(kwargs.get("bot_token")) - self.logs = LOGS - super().__init__(**kwargs) - - @property - def is_bot(self) -> bool: - return self._is_bot - - async def get_me(self, cached: bool = True) -> User: - if not cached or self._me is None: - self._me = await super().get_me() - return self._me - - async def start(self): - try: - self.logs.info("Starting {} Client...".format(self.is_bot and "Bot" or "User")) - await super().start() - except FloodWait as fw: - self.logs.warning(fw) - await sleep(fw.value) - await super().start() - except OperationalError as err: - if str(err) == "database is locked" and os.name == "posix": - self.logs.warning("Session file is locked. Trying to kill blocking process...") - subprocess.run(["fuser", "-k", f"{PROJECT}.session"]) - restart() - raise - except (NotAcceptable, Unauthorized) as err: - self.logs.error(f"{err.__class__.__name__}: {err}\nMoving session file to {PROJECT}.session-old...") - os.rename(f"./{PROJECT}.session", f"./{PROJECT}.session-old") - restart() - except Exception as err: - self.logs.exception(err) - self.logs.error("Client start exiting.") - sys.exit(1) - self.me = await self.get_me() - self.logs.info( - f"Client details:\nID: {self.me.id}\nFirst Name: {self.me.first_name}\nLast Name: {self.me.last_name}\nUsername: {self.me.username}" - ) - done = time_formatter((time() - StartTime) * 1000) - self.logs.success(f">> 🔥 USERBOT IS RUNNING IN {done} !!") - - async def stop(self, *args): - try: - await super().stop() - except BaseException: - pass - - -User = Client( - name=PROJECT, - api_id=Var.API_ID, - api_hash=Var.API_HASH, - session_string=Var.STRING_SESSION, - workers=Var.WORKERS, - workdir=Root, - parse_mode=ParseMode.HTML, - sleep_threshold=30, - plugins={ - "root": PROJECT + ".plugins", - "exclude": [], - }, -) diff --git a/ds/config.py b/ds/config.py index ee92f4a..e887a22 100644 --- a/ds/config.py +++ b/ds/config.py @@ -6,10 +6,10 @@ # < https://github.com/kastaid/ds/blob/main/LICENSE/ >. from os import getenv -import dotenv -from . import WORKERS +from dotenv import load_dotenv, find_dotenv +from ds import WORKERS -dotenv.load_dotenv(dotenv.find_dotenv("config.env")) +load_dotenv(find_dotenv("config.env")) def tobool(val: str) -> int: @@ -34,4 +34,4 @@ class Var: HANDLER: str = getenv("HANDLER", "").strip() -del dotenv, WORKERS +del load_dotenv, find_dotenv, WORKERS diff --git a/ds/helpers.py b/ds/helpers.py index a665090..77950d8 100644 --- a/ds/helpers.py +++ b/ds/helpers.py @@ -7,9 +7,8 @@ import os import sys -from pathlib import Path -from typing import Union -from . import PROJECT, Root +from typing import Union, List +from ds import PROJECT, Root def time_formatter(ms: Union[int, float]) -> str: @@ -17,18 +16,18 @@ def time_formatter(ms: Union[int, float]) -> str: hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) weeks, days = divmod(days, 7) - tmp = ( - ((str(weeks) + "w, ") if weeks else "") - + ((str(days) + "d, ") if days else "") - + ((str(hours) + "h, ") if hours else "") - + ((str(minutes) + "m, ") if minutes else "") - + ((str(seconds) + "s, ") if seconds else "") + time_units = ( + f"{weeks}w, " if weeks else "", + f"{days}d, " if days else "", + f"{hours}h, " if hours else "", + f"{minutes}m, " if minutes else "", + f"{seconds}s, " if seconds else "", ) - return tmp and tmp[:-2] or "0s" + return "".join(time_units)[:-2] or "0s" -def get_terminal_logs() -> Path: - return sorted((Root / "logs").rglob("*.log")) +def get_terminal_logs() -> List[str]: + return sorted(map(str, (Root / "logs").rglob("*.log"))) def restart() -> None: @@ -42,7 +41,5 @@ def restart() -> None: except BaseException: pass reqs = Root / "requirements.txt" - os.system( - f"{sys.executable} -m pip install --disable-pip-version-check --default-timeout=100 --no-cache-dir -U -r {reqs}" - ) + os.system(f"{sys.executable} -m pip install --disable-pip-version-check --default-timeout=100 -U -r {reqs}") os.execl(sys.executable, sys.executable, "-m", PROJECT) diff --git a/ds/logger.py b/ds/logger.py index 7385398..e165159 100644 --- a/ds/logger.py +++ b/ds/logger.py @@ -8,11 +8,11 @@ import logging import sys from datetime import date -from loguru import logger as LOGS -from . import PROJECT +from loguru import logger as LOG +from ds import PROJECT -LOGS.remove(0) -LOGS.add( +LOG.remove(0) +LOG.add( "logs/{}-{}.log".format( PROJECT, date.today().strftime("%Y-%m-%d"), @@ -20,26 +20,26 @@ format="{time:YY/MM/DD HH:mm:ss} | {level: <8} | {name: ^15} | {function: ^15} | {line: >3} : {message}", rotation="1 MB", ) -LOGS.add( +LOG.add( sys.stderr, format="{time:YY/MM/DD HH:mm:ss} | {level} | {name}:{function}:{line} | {message}", level="INFO", colorize=False, ) -LOGS.opt(lazy=True, colors=False) +LOG.opt(lazy=True, colors=False) class InterceptHandler(logging.Handler): def emit(self, record): try: - level = LOGS.level(record.levelname).name + level = LOG.level(record.levelname).name except ValueError: level = record.levelno frame, depth = sys._getframe(6), 6 while frame and frame.f_code.co_filename == logging.__file__: frame = frame.f_back depth += 1 - LOGS.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) + LOG.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage()) logging.disable(logging.DEBUG) diff --git a/ds/patcher.py b/ds/patcher.py new file mode 100644 index 0000000..9dbe6f1 --- /dev/null +++ b/ds/patcher.py @@ -0,0 +1,52 @@ +from asyncio import sleep +import pyrogram +from pyrogram.errors.exceptions.flood_420 import FloodWait +from ds.logger import LOG + + +def patch(obj): + def is_patchable(item): + return getattr(item[1], "patchable", False) + + def wrapper(container): + for name, func in filter(is_patchable, container.__dict__.items()): + setattr(obj, f"old_{name}", getattr(obj, name, None)) + setattr(obj, name, func) + return container + + return wrapper + + +def patchable(func): + func.patchable = True + return func + + +@patch(pyrogram.client.Client) +class Client: + @patchable + async def invoke(self, *args, **kwargs): + try: + return await self.old_invoke(*args, **kwargs) + except FloodWait as fw: + LOG.warning(fw) + await sleep(fw.value) + return await self.invoke(*args, **kwargs) + + @patchable + async def resolve_peer(self, *args, **kwargs): + try: + return await self.old_resolve_peer(*args, **kwargs) + except FloodWait as fw: + LOG.warning(fw) + await sleep(fw.value) + return await self.resolve_peer(*args, **kwargs) + + @patchable + async def save_file(self, *args, **kwargs): + try: + return await self.old_save_file(*args, **kwargs) + except FloodWait as fw: + LOG.warning(fw) + await sleep(fw.value) + return await self.save_file(*args, **kwargs) diff --git a/ds/plugins/delayspam.py b/ds/plugins/delayspam.py index c4d15fe..c1adee1 100644 --- a/ds/plugins/delayspam.py +++ b/ds/plugins/delayspam.py @@ -6,198 +6,101 @@ # < https://github.com/kastaid/ds/blob/main/LICENSE/ >. from asyncio import sleep -from typing import Set, Union +from typing import Dict, Set, Union from pyrogram import filters -from pyrogram.enums.parse_mode import ParseMode -from pyrogram.errors import FloodWait, SlowmodeWait +from pyrogram.enums import ParseMode +from pyrogram.errors import RPCError, SlowmodeWait from pyrogram.types import Message -from ..bot import User -from ..config import Var +from ds.config import Var +from ds.user import UserClient -DS_TASK: Set[int] = set() -DS1_TASK: Set[int] = set() -DS2_TASK: Set[int] = set() -DS3_TASK: Set[int] = set() -DS4_TASK: Set[int] = set() +DS_TASKS: Dict[int, Set[int]] = {i: set() for i in range(10)} -@User.on_message( +def get_task(ds: str) -> Set[int]: + return DS_TASKS.get(int(ds or 0)) + + +@UserClient.on_message( filters.command( - [ - "ds", - "ds1", - "ds2", - "ds3", - "ds4", - ], + [f"ds{i}" if i != 0 else "ds" for i in range(10)], prefixes=Var.HANDLER, ) & filters.me & ~filters.forwarded ) -async def ds_(client: User, m: Message): +async def ds_(c, m): + """ + Start ds, ds1 - ds9 + Usage: ds [delay] [count] [text/reply] + """ chat_id = m.chat.id ds = m.command[0].lower()[2:3] - text = "Please wait until previous •ds{}• finished or cancel it." - if ds == "1": - if chat_id in DS1_TASK: - return await eor(m, text.format(ds), time=2) - elif ds == "2": - if chat_id in DS2_TASK: - return await eor(m, text.format(ds), time=2) - elif ds == "3": - if chat_id in DS3_TASK: - return await eor(m, text.format(ds), time=2) - elif ds == "4": - if chat_id in DS4_TASK: - return await eor(m, text.format(ds), time=2) - else: - if chat_id in DS_TASK: - return await eor(m, text.format(ds), time=2) - if m.reply_to_message_id: - try: - args = m.command[1:] - delay = float(args[0]) - count = int(args[1]) - message = m.reply_to_message - await try_delete(m) - except BaseException: - return await eor(m, f"`{Var.HANDLER}ds{ds} [delay] [count] [reply]`", time=4) - else: + task = get_task(ds) + if chat_id in task: + return await eor(c, m, f"Please wait until previous •ds{ds}• is finished or cancel it.", time=2) + message = m.reply_to_message if m.reply_to_message_id else " ".join(m.text.markdown.split(" ")[3:]) + await c.try_delete(m) + try: + args = m.command[1:] + delay, count = int(args[0]), int(args[1]) + except BaseException: + return await eor(c, m, f"`{Var.HANDLER}ds{ds} [delay] [count] [text/reply]`", time=4) + delay = 2 if int(delay) < 2 else delay + task.add(chat_id) + for _ in range(count): + if chat_id not in get_task(ds): + break try: - await try_delete(m) - args = m.command[1:] - delay = float(args[0]) - count = int(args[1]) - message = ( - m.text.markdown.replace(f"{Var.HANDLER}ds{ds}", "").replace(args[0], "").replace(args[1], "").strip() - ) - except BaseException: - return await eor(m, f"`{Var.HANDLER}ds{ds} [delay] [count] [text]`", time=4) - delay = 2 if delay and int(delay) < 2 else delay - if ds == "1": - DS1_TASK.add(chat_id) - for _ in range(count): - if chat_id not in DS1_TASK: - break - try: - await copy(message, chat_id, delay) - except BaseException: - break - DS1_TASK.discard(chat_id) - elif ds == "2": - DS2_TASK.add(chat_id) - for _ in range(count): - if chat_id not in DS2_TASK: - break - try: - await copy(message, chat_id, delay) - except BaseException: - break - DS2_TASK.discard(chat_id) - elif ds == "3": - DS3_TASK.add(chat_id) - for _ in range(count): - if chat_id not in DS3_TASK: - break - try: - await copy(message, chat_id, delay) - except BaseException: - break - DS3_TASK.discard(chat_id) - elif ds == "4": - DS4_TASK.add(chat_id) - for _ in range(count): - if chat_id not in DS4_TASK: - break - try: - await copy(message, chat_id, delay) - except BaseException: - break - DS4_TASK.discard(chat_id) - else: - DS_TASK.add(chat_id) - for _ in range(count): - if chat_id not in DS_TASK: - break - try: - await copy(message, chat_id, delay) - except BaseException: - break - DS_TASK.discard(chat_id) + await copy(c, message, chat_id, delay) + except SlowmodeWait: + pass + except RPCError: + break + get_task(ds).discard(chat_id) -@User.on_message( +@UserClient.on_message( filters.command( - [ - "dscancel", - "ds1cancel", - "ds2cancel", - "ds3cancel", - "ds4cancel", - ], + [f"ds{i}cancel" if i != 0 else "dscancel" for i in range(10)], prefixes=Var.HANDLER, ) & filters.me & ~filters.forwarded ) -async def dscancel_(_, m: Message): +async def dscancel_(c, m): + """ + Cancel ds - ds9 in current chat + Usage: dscancel, ds1cancel + """ chat_id = m.chat.id ds = m.command[0].lower()[2:3].replace("c", "") - text = "No current •ds{}• are running." - if ds == "1": - if chat_id not in DS1_TASK: - return await eor(m, text.format(ds), time=2) - DS1_TASK.discard(chat_id) - elif ds == "2": - if chat_id not in DS2_TASK: - return await eor(m, text.format(ds), time=2) - DS2_TASK.discard(chat_id) - elif ds == "3": - if chat_id not in DS3_TASK: - return await eor(m, text.format(ds), time=2) - DS3_TASK.discard(chat_id) - elif ds == "4": - if chat_id not in DS3_TASK: - return await eor(m, text.format(ds), time=2) - DS4_TASK.discard(chat_id) - else: - if chat_id not in DS_TASK: - return await eor(m, text.format(ds), time=2) - DS_TASK.discard(chat_id) - await eor(m, f"`ds{ds} cancelled`", time=2) + task = get_task(ds) + if chat_id not in task: + return await eor(c, m, f"No running •ds{ds}• in current chat.", time=2) + task.discard(chat_id) + await eor(c, m, f"`cancelled ds{ds} in current chat`", time=2) -@User.on_message( +@UserClient.on_message( filters.command( - [ - "dsstop", - "ds1stop", - "ds2stop", - "ds3stop", - "ds4stop", - ], + [f"ds{i}stop" if i != 0 else "dsstop" for i in range(10)], prefixes=Var.HANDLER, ) & filters.me & ~filters.forwarded ) -async def dsstop_(_, m: Message): +async def dsstop_(c, m): + """ + Stop ds - ds9 in all chats + usage: dsstop, ds1stop + """ ds = m.command[0].lower()[2:3].replace("s", "") - if ds == "1": - DS1_TASK.clear() - elif ds == "2": - DS2_TASK.clear() - elif ds == "3": - DS3_TASK.clear() - elif ds == "4": - DS4_TASK.clear() - else: - DS_TASK.clear() - await eor(m, f"`stopped all ds{ds}`", time=4) + get_task(ds).clear() + await eor(c, m, f"`stopped ds{ds} in all chats`", time=4) -@User.on_message( +@UserClient.on_message( filters.command( "dsclear", prefixes=Var.HANDLER, @@ -205,47 +108,42 @@ async def dsstop_(_, m: Message): & filters.me & ~filters.forwarded ) -async def dsclear_(_, m: Message): - DS_TASK.clear() - DS1_TASK.clear() - DS2_TASK.clear() - DS3_TASK.clear() - DS4_TASK.clear() - await eor(m, "`clear all ds*`", time=4) +async def dsclear_(c, m): + """ + Clear and stop all ds + usage: dsclear + """ + for task in DS_TASKS.values(): + task.clear() + await eor(c, m, "`clear all ds*`", time=4) async def copy( - message: Union[Message, str], + client, + message: Union[str, Message], chat_id: int, time: Union[int, float], ) -> None: - try: - if isinstance(message, str): - await User.send_message( - chat_id, - message, - parse_mode=ParseMode.DEFAULT, - disable_web_page_preview=True, - disable_notification=True, - ) - else: - await message.copy( - chat_id, - parse_mode=ParseMode.DEFAULT, - disable_notification=True, - reply_to_message_id=None, - ) - await sleep(time) - except FloodWait as fw: - await sleep(fw.value) - await copy(message, chat_id, time) - except SlowmodeWait: - pass - except BaseException: - raise + if isinstance(message, str): + await client.send_message( + chat_id, + message, + parse_mode=ParseMode.DEFAULT, + disable_web_page_preview=True, + disable_notification=True, + ) + else: + await message.copy( + chat_id, + parse_mode=ParseMode.DEFAULT, + disable_notification=True, + reply_to_message_id=None, + ) + await sleep(time) async def eor( + client, message: Message, text: str, time: Union[int, float], @@ -268,15 +166,5 @@ async def eor( ) if not time: return msg - try: - await sleep(time) - return await msg.delete() - except BaseException: - return False - - -async def try_delete(message: Message) -> bool: - try: - return await message.delete() - except BaseException: - return False + await sleep(time) + return await client.try_delete(msg) diff --git a/ds/plugins/misc.py b/ds/plugins/misc.py index 9917d7a..8010de4 100644 --- a/ds/plugins/misc.py +++ b/ds/plugins/misc.py @@ -6,16 +6,17 @@ # < https://github.com/kastaid/ds/blob/main/LICENSE/ >. from asyncio import sleep -from time import time, perf_counter +from time import time, monotonic from pyrogram import filters -from pyrogram.types import Message -from .. import StartTime -from ..bot import User -from ..config import Var -from ..helpers import time_formatter, get_terminal_logs, restart +from pyrogram.errors import RPCError +from pyrogram.raw.functions import Ping +from ds import StartTime +from ds.config import Var +from ds.helpers import time_formatter, get_terminal_logs, restart +from ds.user import UserClient -@User.on_message( +@UserClient.on_message( filters.command( "ping", prefixes=Var.HANDLER, @@ -23,19 +24,23 @@ & filters.me & ~filters.forwarded ) -async def ping_(_, m: Message): - start = perf_counter() - msg = await m.edit("Ping !") - pong = round(perf_counter() - start, 3) +async def ping_(c, m): + start = monotonic() + try: + await c.invoke(Ping(ping_id=0)) + msg = m + except RPCError: + msg = await m.edit("Ping !") + end = monotonic() await msg.edit( - "🏓 Pong !!\nSpeed - {}ms\nUptime - {}".format( - pong, + "🏓 Pong !!\nSpeed - {:.3f}s\nUptime - {}".format( + end - start, time_formatter((time() - StartTime) * 1000), ), ) -@User.on_message( +@UserClient.on_message( filters.command( "restart", prefixes=Var.HANDLER, @@ -43,12 +48,12 @@ async def ping_(_, m: Message): & filters.me & ~filters.forwarded ) -async def restart_(_, m: Message): - await m.edit("Restarting userbot...") +async def restart_(_, m): + await m.edit("Restarting Userbot...") restart() -@User.on_message( +@UserClient.on_message( filters.command( "logs", prefixes=Var.HANDLER, @@ -56,7 +61,7 @@ async def restart_(_, m: Message): & filters.me & ~filters.forwarded ) -async def logs_(_, m: Message): +async def logs_(c, m): msg = await m.edit("Getting logs...") for count, file in enumerate(get_terminal_logs(), start=1): await m.reply_document( @@ -65,4 +70,67 @@ async def logs_(_, m: Message): quote=False, ) await sleep(1) - await msg.delete() + await c.try_delete(msg) + + +@UserClient.on_message( + filters.command( + "id", + prefixes=Var.HANDLER, + ) + & filters.me + & ~filters.forwarded +) +async def id_(_, m): + who = m.reply_to_message.from_user.id if m.reply_to_message_id else m.chat.id + await m.edit(f"{who}") + + +@UserClient.on_message( + filters.command( + "del", + prefixes=Var.HANDLER, + ) + & filters.me + & ~filters.forwarded +) +async def del_(c, m): + await c.try_delete(m) + if m.reply_to_message_id: + await c.try_delete(m.reply_to_message) + + +@UserClient.on_message( + filters.command( + "purge", + prefixes=Var.HANDLER, + ) + & filters.me + & filters.reply + & ~filters.forwarded +) +async def purge_(c, m): + chunk = [] + chat_id, reply_id = m.chat.id, m.reply_to_message.id + async for msg in c.get_chat_history( + chat_id=chat_id, + limit=m.id - reply_id + 1, + ): + if msg.id < reply_id: + break + if msg.from_user.id != c.me.id: + continue + chunk.append(msg.id) + if len(chunk) >= 100: + try: + await c.delete_messages(chat_id, chunk) + except RPCError: + pass + chunk.clear() + await sleep(1) + if len(chunk) > 0: + try: + await c.delete_messages(chat_id, chunk) + except RPCError: + pass + await c.try_delete(m) diff --git a/ds/user.py b/ds/user.py new file mode 100644 index 0000000..2ed8016 --- /dev/null +++ b/ds/user.py @@ -0,0 +1,92 @@ +# ds < https://t.me/kastaid > +# Copyright (C) 2023-present kastaid +# +# This file is a part of < https://github.com/kastaid/ds/ > +# Please read the MIT License in +# < https://github.com/kastaid/ds/blob/main/LICENSE/ >. + +import sys +from platform import version, machine +from time import time +from pyrogram.client import Client +from pyrogram.enums import ParseMode +from pyrogram.errors import RPCError +from pyrogram.types import User, CallbackQuery +from ds import PROJECT, StartTime, Root +from ds.config import Var +from ds.helpers import time_formatter +from ds.logger import LOG + + +class UserClient(Client): + def __init__(self): + self._me: User | None = None + super().__init__( + name=PROJECT, + api_id=Var.API_ID, + api_hash=Var.API_HASH, + session_string=Var.STRING_SESSION, + workers=Var.WORKERS, + workdir=Root, + parse_mode=ParseMode.HTML, + system_version=" ".join((version(), machine())), + plugins={ + "root": "".join((PROJECT, ".plugins")), + "exclude": [], + }, + sleep_threshold=30, + ) + self.log = LOG + + async def get_me( + self, + cached: bool = True, + ) -> User: + if not cached or self._me is None: + self._me = await super().get_me() + return self._me + + async def start(self) -> None: + try: + self.log.info("Starting Userbot Client...") + await super().start() + except Exception as err: + self.log.exception(err) + self.log.error("Userbot Client exiting.") + sys.exit(1) + self.me = await self.get_me() + user_details = f"Userbot Client details:\nID: {self.me.id}\nFirst Name: {self.me.first_name}" + user_details += f"\nLast Name: {self.me.last_name}" if self.me.last_name else "" + user_details += f"\nUsername: {self.me.username}" if self.me.username else "" + self.log.info(user_details) + done = time_formatter((time() - StartTime) * 1000) + self.log.success(f">> 🔥 USERBOT RUNNING IN {done} !!") + + async def answer( + self, + callback, + **args, + ) -> None: + try: + await callback.answer(**args) + except RPCError: + pass + + async def try_delete(self, event) -> bool: + if not event: + return False + is_cb = isinstance(event, CallbackQuery) + message = is_cb and event.message or event + try: + return await message.delete() + except BaseException: + if is_cb: + await self.answer(event) + return False + + async def stop(self, **_) -> None: + try: + await super().stop() + self.log.info("Stopped Client.") + except BaseException: + pass diff --git a/manifest.json b/manifest.json index 9e9976f..311c04f 100644 --- a/manifest.json +++ b/manifest.json @@ -1,3 +1,3 @@ { - "version": "0.0.6" + "version": "0.0.7" } \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index 74c2dee..ff175e6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,8 @@ -black==23.11.0 -isort==5.12.0 -flake8==6.1.0 +black==23.12.1 +isort==5.13.2 +flake8==7.0.0 flake8-builtins==2.2.0 flake8-simplify==0.21.0 flake8-comprehensions==3.14.0 -flake8-bugbear==23.12.2 +flake8-bugbear==24.1.17 flake8-pie==0.16.0 diff --git a/requirements.txt b/requirements.txt index 5b2b515..0d34629 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ loguru==0.7.2 python-dotenv==1.0.0 uvloop==0.19.0 -Pyrogram==2.0.106 +pyrofork==2.3.16.post5 TgCrypto==1.2.5 -psutil==5.9.6 +psutil==5.9.8 diff --git a/setup.cfg b/setup.cfg index b46b808..1490489 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,11 @@ ignore = # Use 'contextlib.suppress(BaseException)' SIM105, # Unnecessary dict comprehension - rewrite using dict() - C416 + C416, + # Star-arg unpacking after a keyword argument is strongly discouraged + B026, + # Don't except `BaseException` unless you plan to re-raise it. + B036 exclude = .git, __pycache__,