Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions ds/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -24,4 +24,4 @@
else:
i.unlink(missing_ok=True)

del Path, rmtree, time
del Path, rmtree, monotonic
7 changes: 5 additions & 2 deletions ds/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
import sys

import uvloop
from pyrogram.sync import compose
from pyrogram import idle

from .kasta import KastaClient
from .logger import LOG
from .patcher import * # noqa


async def main() -> None:
await compose([KastaClient()])
app = KastaClient()
await app.start()
await idle()
await app.stop()


if __name__ == "__main__":
Expand Down
46 changes: 23 additions & 23 deletions ds/config.py
Original file line number Diff line number Diff line change
@@ -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
90 changes: 48 additions & 42 deletions ds/kasta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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))
Expand All @@ -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
17 changes: 5 additions & 12 deletions ds/patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
9 changes: 6 additions & 3 deletions ds/plugins/delayspam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}


Expand Down Expand Up @@ -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,
Expand All @@ -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})
Expand Down
Loading
Loading