├── bot ├── core │ ├── __init__.py │ ├── helper.py │ ├── agents.py │ ├── headers.py │ ├── registrator.py │ ├── launcher.py │ └── tapper.py ├── __init__.py ├── config │ ├── __init__.py │ ├── proxies-template.txt │ └── config.py ├── exceptions │ └── __init__.py └── utils │ ├── loginQR │ ├── src │ │ ├── __init__.py │ │ ├── config.py │ │ ├── args.py │ │ ├── client.py │ │ ├── Colored.py │ │ ├── utils.py │ │ └── updater.py │ ├── __main__.py │ └── __init__.py │ ├── first_run.py │ ├── logger.py │ ├── __init__.py │ ├── async_lock.py │ ├── proxy_utils.py │ ├── config_utils.py │ ├── updater.py │ ├── ad_viewer.py │ ├── web.py │ └── universal_telegram_client.py ├── .dockerignore ├── .python-version ├── .gitattributes ├── docker-compose.yml ├── .gitignore ├── .env-example ├── Dockerfile ├── run.sh ├── requirements.txt ├── main.py ├── run.bat ├── LICENSE ├── pyproject.toml ├── README.md └── README_RU.md /bot/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .venv -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.11 2 | -------------------------------------------------------------------------------- /bot/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.9' -------------------------------------------------------------------------------- /bot/config/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import settings -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | bot/config/proxies.txt merge=skip 2 | docker-compose.yml merge=skip -------------------------------------------------------------------------------- /bot/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | class InvalidSession(Exception): 2 | pass 3 | 4 | class AdViewError(Exception): 5 | pass -------------------------------------------------------------------------------- /bot/config/proxies-template.txt: -------------------------------------------------------------------------------- 1 | type://user:pass@ip:port 2 | type://user:pass:ip:port 3 | type://ip:port:user:pass 4 | type://ip:port@user:pass 5 | type://ip:port 6 | -------------------------------------------------------------------------------- /bot/utils/loginQR/src/__init__.py: -------------------------------------------------------------------------------- 1 | from .updater import raw_handler 2 | from .client import app, args 3 | from .config import APP_ID, APP_HASH 4 | from .utils import check_session, create_qrcodes, nearest -------------------------------------------------------------------------------- /bot/utils/loginQR/src/config.py: -------------------------------------------------------------------------------- 1 | from bot.config.config import settings 2 | 3 | try: 4 | APP_ID = settings.API_ID 5 | APP_HASH = settings.API_HASH 6 | except (NoOptionError, NoSectionError): 7 | print("Find your App configs in https://my.telegram.org") -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | bot: 3 | container_name: 'qlyukerbot' 4 | build: 5 | context: . 6 | stop_signal: SIGINT 7 | restart: unless-stopped 8 | command: "uv run main.py -a 1" 9 | volumes: 10 | - .:/app 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | *.pyo 4 | *.pyd 5 | *.pyi 6 | *.egg-info 7 | dist 8 | sessions/* 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | bot/config/accounts_config.json 15 | bot/config/lock_files 16 | first_run.txt 17 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | 2 | 3 | API_ID = 4 | API_HASH = 5 | GLOBAL_CONFIG_PATH = "TG_FARM" 6 | 7 | FIX_CERT = False 8 | 9 | SESSION_START_DELAY = 360 10 | 11 | REF_ID = 'ref_MjI4NjE4Nzk5' 12 | SESSIONS_PER_PROXY = 1 13 | USE_PROXY = True 14 | DISABLE_PROXY_REPLACE = False 15 | 16 | DEVICE_PARAMS = False 17 | 18 | DEBUG_LOGGING = False 19 | 20 | AUTO_UPDATE = True 21 | CHECK_UPDATE_INTERVAL = 300 22 | BLACKLISTED_SESSIONS = "" 23 | -------------------------------------------------------------------------------- /bot/utils/loginQR/__main__.py: -------------------------------------------------------------------------------- 1 | from .src import app, check_session, create_qrcodes, nearest, raw_handler 2 | from pyrogram import idle, handlers 3 | import asyncio 4 | 5 | async def main(): 6 | await check_session(app, nearest.nearest_dc) 7 | await create_qrcodes() 8 | await idle() 9 | 10 | app.add_handler( 11 | handlers.RawUpdateHandler( 12 | raw_handler 13 | ) 14 | ) 15 | 16 | loop = asyncio.get_event_loop() 17 | loop.run_until_complete(main()) -------------------------------------------------------------------------------- /bot/core/helper.py: -------------------------------------------------------------------------------- 1 | def format_duration(seconds: float) -> str: 2 | if seconds < 0: 3 | return "0s" 4 | 5 | hours = int(seconds // 3600) 6 | minutes = int((seconds % 3600) // 60) 7 | secs = int(seconds % 60) 8 | 9 | parts = [] 10 | if hours > 0: 11 | parts.append(f"{hours}h") 12 | if minutes > 0: 13 | parts.append(f"{minutes}m") 14 | if secs > 0 or not parts: 15 | parts.append(f"{secs}s") 16 | 17 | return " ".join(parts) -------------------------------------------------------------------------------- /bot/core/agents.py: -------------------------------------------------------------------------------- 1 | from ua_generator import generate 2 | from ua_generator.options import Options 3 | from ua_generator.data.version import VersionRange 4 | 5 | def generate_random_user_agent(platform: str = 'android', browser: str = 'chrome', 6 | min_version: int = 110, max_version: int = 129) -> str: 7 | options = Options(version_ranges={'chrome': VersionRange(min_version, max_version)}) 8 | return generate(browser=browser, platform=platform, options=options).text -------------------------------------------------------------------------------- /bot/utils/first_run.py: -------------------------------------------------------------------------------- 1 | import aiofiles 2 | 3 | async def check_is_first_run(session_name: str): 4 | async with aiofiles.open('first_run.txt', mode='a+') as file: 5 | await file.seek(0) 6 | lines = await file.readlines() 7 | return session_name.lower() not in [line.strip() for line in lines] 8 | 9 | async def append_recurring_session(session_name: str): 10 | async with aiofiles.open('first_run.txt', mode='a+') as file: 11 | await file.writelines(session_name.lower() + '\n') -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim 2 | 3 | ENV PYTHONUNBUFFERED=1 4 | ENV TERM=xterm-256color 5 | ENV FORCE_COLOR=1 6 | ENV UV_SYSTEM_PYTHON=1 7 | ENV UV_NETWORK_TIMEOUT=60 8 | ENV UV_RETRY_ATTEMPTS=5 9 | 10 | ENV HOSTALIASES=/etc/host.aliases 11 | ENV UV_CUSTOM_PYPI_URL=https://pypi.org/simple/ 12 | 13 | RUN apt-get update && apt-get install -y \ 14 | build-essential \ 15 | qtbase5-dev \ 16 | qt5-qmake \ 17 | qtchooser \ 18 | bash \ 19 | git \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | WORKDIR /app/ 23 | 24 | COPY . . 25 | 26 | CMD ["uv", "run", "main.py", "-a", "1"] 27 | -------------------------------------------------------------------------------- /bot/utils/loginQR/src/args.py: -------------------------------------------------------------------------------- 1 | args = [ 2 | { 3 | 'short_name': '-s', 4 | 'long_name': '--session_name', 5 | 'help': 'any name you want to give to your pyrogram.Client', 6 | 'type': str 7 | }, 8 | { 9 | 'short_name': '-v', 10 | 'long_name': '--app_version', 11 | 'help': 'a string classify as version to give to your pyrogram.Client', 12 | 'type': str 13 | }, 14 | { 15 | 'short_name': '-t', 16 | 'long_name': '--token', 17 | 'help': 'if generating session is for a bot to pass bot_token', 18 | 'type': str 19 | }, 20 | 21 | ] -------------------------------------------------------------------------------- /bot/utils/loginQR/src/client.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .config import APP_HASH, APP_ID 3 | from pyrogram import Client 4 | from .Colored import ColoredArgParser 5 | from .args import args as Args 6 | 7 | SESSIONS_DIR = "sessions" 8 | 9 | if not os.path.exists(SESSIONS_DIR): 10 | os.makedirs(SESSIONS_DIR) 11 | 12 | parser = ColoredArgParser() 13 | for arg in Args: 14 | parser.add_argument( 15 | arg['short_name'], 16 | arg['long_name'], 17 | help=arg['help'], 18 | type=arg['type'] 19 | ) 20 | args = parser.parse_args() 21 | 22 | session_path = os.path.join(SESSIONS_DIR, args.session_name or "pyrogram") 23 | 24 | app = Client(session_path, api_id=APP_ID, api_hash=APP_HASH) -------------------------------------------------------------------------------- /bot/utils/loginQR/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from pyrogram import Client, errors 4 | from .src import args, app, APP_ID, APP_HASH 5 | from rich.logging import RichHandler 6 | 7 | FORMAT = "%(message)s" 8 | logging.basicConfig( 9 | level=logging.WARNING, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] 10 | ) 11 | 12 | if args.token: 13 | app = Client( 14 | args.session_name or "pyrogram", 15 | api_id=APP_ID, 16 | api_hash=APP_HASH, 17 | bot_token=args.token 18 | ) 19 | try: 20 | app.start() 21 | except ( 22 | errors.AccessTokenInvalid, 23 | errors.AccessTokenExpired, 24 | errors.AuthKeyUnregistered 25 | ) as err: 26 | sys.exit(print(err)) 27 | me = app.get_me().first_name 28 | session_string = app.export_session_string() 29 | app.stop() 30 | 31 | sys.exit( 32 | 33 | ) -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | first_run=true 4 | 5 | check_uv() { 6 | if ! command -v uv &> /dev/null; then 7 | echo "uv is not installed. Installing..." 8 | curl -LsSf https://astral.sh/uv/install.sh | sh 9 | 10 | echo "Reloading shell to update PATH..." 11 | exec "$SHELL" 12 | fi 13 | } 14 | 15 | if [ ! -f ".env" ]; then 16 | echo "Copying configuration file..." 17 | cp .env-example .env 18 | fi 19 | 20 | check_uv 21 | 22 | while true; do 23 | echo "Checking for updates..." 24 | git fetch 25 | git pull 26 | 27 | if [ "$first_run" = true ]; then 28 | echo "Starting the application for the first time..." 29 | uv run main.py 30 | first_run=false 31 | else 32 | echo "Restarting the application..." 33 | uv run main.py -a 1 34 | fi 35 | 36 | echo "Restarting program in 10 seconds..." 37 | sleep 10 38 | done 39 | -------------------------------------------------------------------------------- /bot/utils/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from loguru import logger 3 | from bot.config import settings 4 | from datetime import date 5 | 6 | logger.remove() 7 | 8 | logger.add( 9 | sink=sys.stdout, 10 | format="{time:YYYY-MM-DD HH:mm:ss}" 11 | " | {level: <8}" 12 | " | {message}", 13 | filter=lambda record: record["level"].name != "TRACE", 14 | colorize=True 15 | ) 16 | 17 | if settings.DEBUG_LOGGING: 18 | logger.add( 19 | f"logs/err_tracebacks_{date.today()}.txt", 20 | format="{time:DD.MM.YYYY HH:mm:ss} - {level} - {message}", 21 | level="TRACE", 22 | backtrace=True, 23 | diagnose=True, 24 | filter=lambda record: record["level"].name == "TRACE" 25 | ) 26 | 27 | logger = logger.opt(colors=True) 28 | 29 | def log_error(text: str) -> None: 30 | if settings.DEBUG_LOGGING: 31 | logger.opt(exception=True).trace(text) 32 | logger.error(text) -------------------------------------------------------------------------------- /bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .logger import logger, log_error 4 | from .async_lock import AsyncInterProcessLock 5 | from . import proxy_utils, config_utils, first_run 6 | from bot.config import settings 7 | 8 | if not os.path.isdir(settings.GLOBAL_CONFIG_PATH): 9 | GLOBAL_CONFIG_PATH = os.environ.get(settings.GLOBAL_CONFIG_PATH, "") 10 | else: 11 | GLOBAL_CONFIG_PATH = settings.GLOBAL_CONFIG_PATH 12 | GLOBAL_CONFIG_EXISTS = os.path.isdir(GLOBAL_CONFIG_PATH) 13 | 14 | CONFIG_PATH = os.path.join(GLOBAL_CONFIG_PATH, 'accounts_config.json') if GLOBAL_CONFIG_EXISTS else 'bot/config/accounts_config.json' 15 | SESSIONS_PATH = os.path.join(GLOBAL_CONFIG_PATH, 'sessions') if GLOBAL_CONFIG_EXISTS else 'sessions' 16 | PROXIES_PATH = os.path.join(GLOBAL_CONFIG_PATH, 'proxies.txt') if GLOBAL_CONFIG_EXISTS else 'bot/config/proxies.txt' 17 | 18 | if not os.path.exists(path=SESSIONS_PATH): 19 | os.mkdir(path=SESSIONS_PATH) 20 | 21 | if settings.FIX_CERT: 22 | from certifi import where 23 | os.environ['SSL_CERT_FILE'] = where() -------------------------------------------------------------------------------- /bot/config/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | from typing import List, Dict, Tuple 3 | from enum import Enum 4 | 5 | class Settings(BaseSettings): 6 | model_config = SettingsConfigDict(env_file=".env", env_ignore_empty=True) 7 | 8 | API_ID: int = None 9 | API_HASH: str = None 10 | GLOBAL_CONFIG_PATH: str = "TG_FARM" 11 | 12 | FIX_CERT: bool = False 13 | 14 | SESSION_START_DELAY: int = 360 15 | 16 | REF_ID: str = 'bro-228618799' 17 | SESSIONS_PER_PROXY: int = 1 18 | USE_PROXY: bool = True 19 | DISABLE_PROXY_REPLACE: bool = False 20 | 21 | DEVICE_PARAMS: bool = False 22 | 23 | DEBUG_LOGGING: bool = False 24 | 25 | AUTO_UPDATE: bool = True 26 | CHECK_UPDATE_INTERVAL: int = 60 27 | BLACKLISTED_SESSIONS: str = "" 28 | 29 | SUBSCRIBE_TELEGRAM: bool = True 30 | 31 | @property 32 | def blacklisted_sessions(self) -> List[str]: 33 | return [s.strip() for s in self.BLACKLISTED_SESSIONS.split(',') if s.strip()] 34 | 35 | settings = Settings() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiocfscrape==1.0.0 2 | aiofiles==24.1.0 3 | aiohttp==3.9.5 4 | aiohttp-proxy==0.1.2 5 | aiosignal==1.3.1 6 | annotated-types==0.7.0 7 | asyncio==3.4.3 8 | attrs==24.2.0 9 | better-proxy==1.2.0 10 | blinker==1.9.0 11 | certifi==2024.8.30 12 | click==8.1.7 13 | colorama==0.4.6 14 | fasteners==0.19 15 | Flask==3.1.0 16 | frozenlist==1.5.0 17 | idna==3.10 18 | itsdangerous==2.2.0 19 | Jinja2==3.1.4 20 | Js2Py==0.74 21 | loguru==0.7.2 22 | MarkupSafe==3.0.2 23 | multidict==6.1.0 24 | opentele==1.15.1 25 | propcache==0.2.0 26 | pyaes==1.6.1 27 | pyasn1==0.6.1 28 | pydantic-settings==2.4.0 29 | pyjsparser==2.7.1 30 | PyQt5_sip 31 | PyQt5 32 | PyQt5-Qt5 33 | Pyrogram==2.0.106 34 | PySocks==1.7.1 35 | python-dotenv==1.0.1 36 | python-socks==2.5.1 37 | qrcode==8.0 38 | rsa==4.9 39 | six==1.16.0 40 | Telethon==1.36.0 41 | TgCrypto==1.2.5 42 | typing_extensions==4.12.2 43 | tzlocal==5.2 44 | ua-generator==1.0.6 45 | Werkzeug==3.1.3 46 | yarl==1.18.0 47 | SQLAlchemy==2.0.28 48 | alembic==1.13.1 49 | pandas==2.2.0 50 | matplotlib==3.8.2 51 | pytest==7.4.4 52 | python-dateutil==2.8.2 53 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import suppress 3 | from bot.core.launcher import process 4 | from os import system, name as os_name, environ 5 | import os 6 | 7 | def is_docker() -> bool: 8 | path = '/proc/self/cgroup' 9 | return os.path.exists('/.dockerenv') or (os.path.isfile(path) and any('docker' in line for line in open(path))) 10 | 11 | def can_set_title() -> bool: 12 | if is_docker(): 13 | return False 14 | 15 | term = environ.get('TERM', '') 16 | if term in ('', 'dumb', 'unknown'): 17 | return False 18 | 19 | return True 20 | 21 | def set_window_title(title: str) -> None: 22 | if not can_set_title(): 23 | return 24 | try: 25 | if os_name == 'nt': 26 | system(f'title {title}') 27 | else: 28 | print(f'\033]0;{title}\007', end='', flush=True) 29 | except Exception: 30 | pass 31 | 32 | async def main() -> None: 33 | await process() 34 | 35 | if __name__ == '__main__': 36 | set_window_title('NAME') 37 | with suppress(KeyboardInterrupt): 38 | asyncio.run(main()) -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set firstRun=true 4 | 5 | if not exist venv ( 6 | echo Creating virtual environment... 7 | python -m venv venv 8 | ) 9 | 10 | echo Activating virtual environment... 11 | call venv\Scripts\activate 12 | 13 | if not exist venv\Lib\site-packages\installed ( 14 | if exist requirements.txt ( 15 | echo installing wheel for faster installing 16 | pip install wheel 17 | echo Installing dependencies... 18 | pip install -r requirements.txt 19 | echo. > venv\Lib\site-packages\installed 20 | ) else ( 21 | echo requirements.txt not found, skipping dependency installation. 22 | ) 23 | ) else ( 24 | echo Dependencies already installed, skipping installation. 25 | ) 26 | 27 | if not exist .env ( 28 | echo Copying configuration file 29 | copy .env-example .env 30 | ) else ( 31 | echo Skipping .env copying 32 | ) 33 | 34 | echo Starting the bot... 35 | :loop 36 | git fetch 37 | git pull 38 | if "%firstRun%"=="true" ( 39 | python main.py 40 | set firstRun=false 41 | ) else ( 42 | python main.py -a 1 43 | ) 44 | echo Restarting the program in 10 seconds... 45 | timeout /t 10 /nobreak >nul 46 | goto :loop 47 | -------------------------------------------------------------------------------- /bot/utils/async_lock.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import fasteners 3 | from random import uniform 4 | from os import path 5 | 6 | from bot.utils import logger 7 | 8 | class AsyncInterProcessLock: 9 | def __init__(self, lock_file: str): 10 | self._lock = fasteners.InterProcessLock(lock_file) 11 | self._file_name, _ = path.splitext(path.basename(lock_file)) 12 | 13 | async def __aenter__(self) -> 'AsyncInterProcessLock': 14 | while True: 15 | lock_acquired = await asyncio.to_thread(self._lock.acquire, timeout=uniform(5, 10)) 16 | if lock_acquired: 17 | return self 18 | sleep_time = uniform(30, 150) 19 | logger_message = ( 20 | f"{self._file_name} | Failed to acquire lock for " 21 | f"{'accounts_config' if 'accounts_config' in self._file_name else 'session'}. " 22 | f"Retrying in {int(sleep_time)} seconds" 23 | ) 24 | logger.info(logger_message) 25 | await asyncio.sleep(sleep_time) 26 | 27 | async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: 28 | await asyncio.to_thread(self._lock.release) -------------------------------------------------------------------------------- /bot/core/headers.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | HEADERS = { 4 | 'accept': 'application/json', 5 | 'accept-language': 'ru,en-US;q=0.9,en;q=0.8', 6 | 'cache-control': 'no-cache', 7 | 'content-type': 'application/json', 8 | 'dnt': '1', 9 | 'origin': 'https://bitappprod.com', 10 | 'pragma': 'no-cache', 11 | 'priority': 'u=1, i', 12 | 'referer': 'https://bitappprod.com/', 13 | 'sec-ch-ua': '"Chromium";v="131", "Not_A Brand";v="24"', 14 | 'sec-ch-ua-mobile': '?0', 15 | 'sec-ch-ua-platform': '"macOS"', 16 | 'sec-fetch-dest': 'empty', 17 | 'sec-fetch-mode': 'cors', 18 | 'sec-fetch-site': 'same-origin', 19 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 20 | 'x-device-model': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', 21 | 'x-device-platform': 'android' 22 | } 23 | 24 | def get_auth_headers(token: str) -> Dict[str, str]: 25 | auth_headers = HEADERS.copy() 26 | auth_headers['authorization'] = f'Bearer {token}' 27 | auth_headers['lang'] = 'en' 28 | return auth_headers -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Artem Mykhailichenko 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /bot/utils/loginQR/src/Colored.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from gettext import gettext 4 | 5 | class ColoredArgParser(argparse.ArgumentParser): 6 | 7 | color_dict = {'RED' : '1;31', 'GREEN' : '1;32', 8 | 'YELLOW' : '1;33', 'BLUE' : '1;36'} 9 | 10 | def print_usage(self, file = None): 11 | if file is None: 12 | file = sys.stdout 13 | self._print_message(self.format_usage()[0].upper() + 14 | self.format_usage()[1:], 15 | file, self.color_dict['YELLOW']) 16 | 17 | def print_help(self, file = None): 18 | if file is None: 19 | file = sys.stdout 20 | self._print_message(self.format_help()[0].upper() + 21 | self.format_help()[1:], 22 | file, self.color_dict['BLUE']) 23 | 24 | def _print_message(self, message, file = None, color = None): 25 | if message: 26 | if file is None: 27 | file = sys.stderr 28 | 29 | if color is None: 30 | file.write(message) 31 | else: 32 | 33 | file.write('\x1b[' + color + 'm' + message.strip() + '\x1b[0m\n') 34 | 35 | def exit(self, status = 0, message = None): 36 | if message: 37 | self._print_message(message, sys.stderr, self.color_dict['RED']) 38 | sys.exit(status) 39 | 40 | def error(self, message): 41 | self.print_usage(sys.stderr) 42 | args = {'prog' : self.prog, 'message': message} 43 | self.exit(2, gettext('%(prog)s: Error: %(message)s\n') % args) -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "qlyukerbot2" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "aiocfscrape==1.0.0", 9 | "aiofiles==24.1.0", 10 | "aiohttp==3.9.5", 11 | "aiohttp-proxy==0.1.2", 12 | "aiosignal==1.3.1", 13 | "alembic==1.13.1", 14 | "annotated-types==0.7.0", 15 | "asyncio==3.4.3", 16 | "attrs==24.2.0", 17 | "better-proxy==1.2.0", 18 | "blinker==1.9.0", 19 | "certifi==2024.8.30", 20 | "click==8.1.7", 21 | "colorama==0.4.6", 22 | "fasteners==0.19", 23 | "flask==3.1.0", 24 | "frozenlist==1.5.0", 25 | "idna==3.10", 26 | "itsdangerous==2.2.0", 27 | "jinja2==3.1.4", 28 | "js2py==0.74", 29 | "loguru==0.7.2", 30 | "markupsafe==3.0.2", 31 | "matplotlib==3.8.2", 32 | "multidict==6.1.0", 33 | "opentele==1.15.1", 34 | "pandas==2.2.0", 35 | "propcache==0.2.0", 36 | "pyaes==1.6.1", 37 | "pyasn1==0.6.1", 38 | "pydantic-settings==2.4.0", 39 | "pyjsparser==2.7.1", 40 | "pyqt5>=5.15.11", 41 | "pyqt5-qt5>=5.15.16", 42 | "pyqt5-sip>=12.17.0", 43 | "pyrogram==2.0.106", 44 | "pysocks==1.7.1", 45 | "pytest==7.4.4", 46 | "python-dateutil==2.8.2", 47 | "python-dotenv==1.0.1", 48 | "python-socks==2.5.1", 49 | "qrcode==8.0", 50 | "rsa==4.9", 51 | "six==1.16.0", 52 | "sqlalchemy==2.0.28", 53 | "telethon==1.36.0", 54 | "tgcrypto==1.2.5", 55 | "typing-extensions==4.12.2", 56 | "tzlocal==5.2", 57 | "ua-generator==1.0.6", 58 | "werkzeug==3.1.3", 59 | "yarl==1.18.0", 60 | ] 61 | -------------------------------------------------------------------------------- /bot/utils/loginQR/src/utils.py: -------------------------------------------------------------------------------- 1 | from pyrogram.session import Session, Auth 2 | from pyrogram import Client, raw 3 | import asyncio 4 | import os 5 | from qrcode import QRCode 6 | from base64 import urlsafe_b64encode as base64url 7 | from subprocess import call 8 | from .config import APP_ID, APP_HASH 9 | from .client import app 10 | 11 | qr = QRCode() 12 | 13 | async def check_session(client: Client, dc_id: int): 14 | if not isinstance(dc_id, int): 15 | raise ValueError(f"Error: dc_id must be an integer, but got {type(dc_id)}") 16 | 17 | await client.session.stop() 18 | await client.storage.dc_id(dc_id) 19 | 20 | test_mode = bool(await client.storage.test_mode()) 21 | 22 | await client.storage.auth_key( 23 | await Auth( 24 | client, await client.storage.dc_id(), 25 | test_mode 26 | ).create() 27 | ) 28 | 29 | client.session = Session( 30 | client, await client.storage.dc_id(), 31 | await client.storage.auth_key(), test_mode 32 | ) 33 | return await client.session.start() 34 | 35 | async def clear_screen(): 36 | call(['cls' if os.name == 'nt' else 'clear'], shell=True) 37 | 38 | async def create_qrcodes(): 39 | if not app.is_initialized: 40 | await app.dispatcher.start() 41 | app.is_initialized = True 42 | while True: 43 | await clear_screen() 44 | print( 45 | 'Scan the QR code below:' 46 | ) 47 | print( 48 | 'Settings > Privacy and Security > Active Sessions > Scan QR Code' 49 | ) 50 | r = await app.invoke( 51 | raw.functions.auth.ExportLoginToken( 52 | api_id=APP_ID, api_hash=str(APP_HASH), except_ids=[] 53 | ) 54 | ) 55 | if isinstance(r, raw.types.auth.LoginToken): 56 | await _gen_qr(r.token) 57 | await asyncio.sleep(30) 58 | 59 | async def _gen_qr(token: bytes): 60 | token = base64url(token).decode("utf8") 61 | login_url = f"tg://login?token={token}" 62 | qr.clear() 63 | qr.add_data(login_url) 64 | qr.print_ascii() 65 | 66 | app.connect() 67 | nearest = app.invoke(raw.functions.help.GetNearestDc()) -------------------------------------------------------------------------------- /bot/utils/loginQR/src/updater.py: -------------------------------------------------------------------------------- 1 | from .config import APP_ID, APP_HASH 2 | from pyrogram import Client, raw, errors, utils 3 | import asyncio 4 | from .utils import check_session, clear_screen, _gen_qr, nearest 5 | import sys 6 | 7 | ACCEPTED = False 8 | 9 | async def raw_handler(client: Client, update: raw.base.Update, users: list, chats: list): 10 | if isinstance(update, raw.types.auth.LoginToken) and nearest.nearest_dc != await client.storage.dc_id(): 11 | await check_session(client, dc_id=nearest.nearest_dc) 12 | 13 | if isinstance(update, raw.types.UpdateLoginToken): 14 | try: 15 | r = await client.invoke( 16 | raw.functions.auth.ExportLoginToken( 17 | api_id=APP_ID, api_hash=APP_HASH, except_ids=[] 18 | ) 19 | ) 20 | except errors.exceptions.unauthorized_401.SessionPasswordNeeded as err: 21 | await client.check_password(await utils.ainput("2FA Password: ", hide=True)) 22 | r = await client.invoke( 23 | raw.functions.auth.ExportLoginToken( 24 | api_id=APP_ID, api_hash=APP_HASH, except_ids=[] 25 | ) 26 | ) 27 | 28 | if isinstance(r, raw.types.auth.LoginTokenSuccess): 29 | me = await client.get_me() 30 | try: 31 | dc_id = await client.storage.dc_id() 32 | auth_key = await client.storage.auth_key() 33 | test_mode = bool(await client.storage.test_mode()) 34 | user_id = me.id 35 | is_bot = me.is_bot 36 | 37 | await client.storage.user_id(user_id) 38 | await client.storage.is_bot(is_bot) 39 | 40 | if not isinstance(dc_id, int): 41 | raise ValueError(f"Error: dc_id must be an integer, but got {type(dc_id)}") 42 | if not isinstance(test_mode, bool): 43 | raise ValueError(f"Error: test_mode must be a boolean, but got {type(test_mode)}") 44 | if not isinstance(auth_key, bytes): 45 | raise ValueError(f"Error: auth_key must be bytes, but got {type(auth_key)}") 46 | if not isinstance(user_id, int): 47 | raise ValueError(f"Error: user_id must be an integer, but got {type(user_id)}") 48 | if not isinstance(is_bot, bool): 49 | raise ValueError(f"Error: is_bot must be a boolean, but got {type(is_bot)}") 50 | 51 | session_string = await client.export_session_string() 52 | 53 | sys.exit( 54 | 55 | ) 56 | except Exception as e: 57 | print(f"Error generating session string: {e}") -------------------------------------------------------------------------------- /bot/core/registrator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from better_proxy import Proxy 3 | from telethon import TelegramClient 4 | from pyrogram import Client 5 | from bot.config import settings 6 | from bot.utils import logger, proxy_utils, config_utils, CONFIG_PATH, PROXIES_PATH, SESSIONS_PATH 7 | 8 | API_ID = settings.API_ID 9 | API_HASH = settings.API_HASH 10 | 11 | async def register_sessions() -> None: 12 | if not API_ID or not API_HASH: 13 | raise ValueError("API_ID and API_HASH not found in the .env file.") 14 | 15 | session_name = input('\nEnter the session name (press Enter to exit): ').strip() 16 | if not session_name: 17 | return None 18 | 19 | session_file = f"{session_name}.session" 20 | device_params = {} 21 | 22 | if settings.DEVICE_PARAMS: 23 | device_params.update( 24 | { 25 | 'device_model': input('device_model: ').strip(), 26 | 'system_version': input('system_version: ').strip(), 27 | 'app_version': input('app_version: ').strip() 28 | } 29 | ) 30 | accounts_config = config_utils.read_config_file(CONFIG_PATH) 31 | accounts_data = { 32 | "api": { 33 | 'api_id': API_ID, 34 | 'api_hash': API_HASH, 35 | **device_params 36 | } 37 | } 38 | proxy = None 39 | 40 | if settings.USE_PROXY: 41 | proxies = proxy_utils.get_unused_proxies(accounts_config, PROXIES_PATH) 42 | if not proxies: 43 | raise Exception('No unused proxies left') 44 | for prox in proxies: 45 | if await proxy_utils.check_proxy(prox): 46 | proxy_str = prox 47 | proxy = Proxy.from_str(proxy_str) 48 | accounts_data['proxy'] = proxy_str 49 | break 50 | else: 51 | raise Exception('No unused proxies left') 52 | accounts_data['proxy'] = None 53 | 54 | accounts_config[session_name] = accounts_data 55 | while True: 56 | res = input('Which session to create?\n1. Telethon\n2. Pyrogram\n').strip() 57 | if res not in ['1', '2']: 58 | logger.warning("Invalid option. Please enter 1 or 2") 59 | else: 60 | break 61 | if res == '1': 62 | session = TelegramClient( 63 | os.path.join(SESSIONS_PATH, session_file), 64 | api_id=API_ID, 65 | api_hash=API_HASH, 66 | lang_code="en", 67 | system_lang_code="en-US", 68 | **device_params 69 | ) 70 | if proxy: 71 | session.set_proxy(proxy_utils.to_telethon_proxy(proxy)) 72 | 73 | await session.start() 74 | user_data = await session.get_me() 75 | 76 | else: 77 | session = Client( 78 | os.path.join(SESSIONS_PATH, session_file), 79 | api_id=API_ID, 80 | api_hash=API_HASH, 81 | lang_code="en", 82 | **device_params 83 | ) 84 | if proxy: 85 | session.proxy = proxy_utils.to_pyrogram_proxy(proxy) 86 | 87 | await session.start() 88 | user_data = await session.get_me() 89 | 90 | if user_data: 91 | await config_utils.write_config_file(accounts_config, CONFIG_PATH) 92 | logger.success( 93 | f'Session added successfully @{user_data.username} | {user_data.first_name} {user_data.last_name}' 94 | ) -------------------------------------------------------------------------------- /bot/utils/proxy_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import aiohttp 3 | from aiohttp_proxy import ProxyConnector 4 | from collections import Counter 5 | from python_socks import ProxyType 6 | from shutil import copyfile 7 | from better_proxy import Proxy 8 | from bot.config import settings 9 | from bot.utils import logger 10 | from random import shuffle 11 | 12 | PROXY_TYPES = { 13 | 'socks5': ProxyType.SOCKS5, 14 | 'socks4': ProxyType.SOCKS4, 15 | 'http': ProxyType.HTTP, 16 | 'https': ProxyType.HTTP 17 | } 18 | 19 | def get_proxy_type(proxy_type: str) -> ProxyType: 20 | return PROXY_TYPES.get(proxy_type.lower()) 21 | 22 | def to_telethon_proxy(proxy: Proxy) -> dict: 23 | return { 24 | 'proxy_type': get_proxy_type(proxy.protocol), 25 | 'addr': proxy.host, 26 | 'port': proxy.port, 27 | 'username': proxy.login, 28 | 'password': proxy.password 29 | } 30 | 31 | def to_pyrogram_proxy(proxy: Proxy) -> dict: 32 | return { 33 | 'scheme': proxy.protocol if proxy.protocol != 'https' else 'http', 34 | 'hostname': proxy.host, 35 | 'port': proxy.port, 36 | 'username': proxy.login, 37 | 'password': proxy.password 38 | } 39 | 40 | def get_proxies(proxy_path: str) -> list[str]: 41 | proxy_template_path = "bot/config/proxies-template.txt" 42 | 43 | if not os.path.isfile(proxy_path): 44 | copyfile(proxy_template_path, proxy_path) 45 | return [] 46 | 47 | if settings.USE_PROXY: 48 | with open(file=proxy_path, encoding="utf-8-sig") as file: 49 | return list({Proxy.from_str(proxy=row.strip()).as_url for row in file if row.strip() and 50 | not row.strip().startswith('type')}) 51 | return [] 52 | 53 | def get_unused_proxies(accounts_config: dict, proxy_path: str) -> list[str]: 54 | proxies_count = Counter([v.get('proxy') for v in accounts_config.values() if v.get('proxy')]) 55 | all_proxies = get_proxies(proxy_path) 56 | return [proxy for proxy in all_proxies if proxies_count.get(proxy, 0) < settings.SESSIONS_PER_PROXY] 57 | 58 | async def check_proxy(proxy: str) -> bool: 59 | url = 'https://ifconfig.me/ip' 60 | proxy_conn = ProxyConnector.from_url(proxy) 61 | try: 62 | async with aiohttp.ClientSession(connector=proxy_conn, timeout=aiohttp.ClientTimeout(15)) as session: 63 | response = await session.get(url) 64 | if response.status == 200: 65 | logger.success(f"Successfully connected to proxy. IP: {await response.text()}") 66 | if not proxy_conn.closed: 67 | proxy_conn.close() 68 | return True 69 | except Exception: 70 | logger.warning(f"Proxy {proxy} didn't respond") 71 | return False 72 | 73 | async def get_proxy_chain(path: str) -> tuple[str | None, str | None]: 74 | try: 75 | with open(path, 'r') as file: 76 | proxy = file.read().strip() 77 | return proxy, to_telethon_proxy(Proxy.from_str(proxy)) 78 | except Exception: 79 | logger.error(f"Failed to get proxy for proxy chain from '{path}'") 80 | return None, None 81 | 82 | async def get_working_proxy(accounts_config: dict, current_proxy: str | None) -> str | None: 83 | if current_proxy and await check_proxy(current_proxy): 84 | return current_proxy 85 | 86 | from bot.utils import PROXIES_PATH 87 | unused_proxies = get_unused_proxies(accounts_config, PROXIES_PATH) 88 | shuffle(unused_proxies) 89 | for proxy in unused_proxies: 90 | if await check_proxy(proxy): 91 | return proxy 92 | 93 | return None -------------------------------------------------------------------------------- /bot/utils/config_utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from bot.utils import logger, log_error, AsyncInterProcessLock 4 | from opentele.api import API 5 | from os import path, remove 6 | from copy import deepcopy 7 | 8 | def read_config_file(config_path: str) -> dict: 9 | try: 10 | with open(config_path, 'r') as file: 11 | content = file.read() 12 | return json.loads(content) if content else {} 13 | except FileNotFoundError: 14 | with open(config_path, 'w'): 15 | logger.warning(f"Accounts config file `{config_path}` not found. Creating a new one.") 16 | return {} 17 | 18 | async def write_config_file(content: dict, config_path: str) -> None: 19 | lock = AsyncInterProcessLock(path.join(path.dirname(config_path), 'lock_files', 'accounts_config.lock')) 20 | async with lock: 21 | with open(config_path, 'w+') as file: 22 | json.dump(content, file, indent=2) 23 | await asyncio.sleep(0.1) 24 | 25 | def get_session_config(session_name: str, config_path: str) -> dict: 26 | return read_config_file(config_path).get(session_name, {}) 27 | 28 | async def update_session_config_in_file(session_name: str, updated_session_config: dict, config_path: str) -> None: 29 | config = read_config_file(config_path) 30 | config[session_name] = updated_session_config 31 | await write_config_file(config, config_path) 32 | 33 | async def restructure_config(config_path: str) -> None: 34 | config = read_config_file(config_path) 35 | if config: 36 | cfg_copy = deepcopy(config) 37 | for key, value in cfg_copy.items(): 38 | api_info = { 39 | "api_id": value.get('api', {}).get("api_id") or value.pop("api_id", None), 40 | "api_hash": value.get('api', {}).get("api_hash") or value.pop("api_hash", None), 41 | "device_model": value.get('api', {}).get("device_model") or value.pop("device_model", None), 42 | "system_version": value.get('api', {}).get("system_version") or value.pop("system_version", None), 43 | "app_version": value.get('api', {}).get("app_version") or value.pop("app_version", None), 44 | "system_lang_code": value.get('api', {}).get("system_lang_code") or value.pop("system_lang_code", None), 45 | "lang_pack": value.get('api', {}).get("lang_pack") or value.pop("lang_pack", None), 46 | "lang_code": value.get('api', {}).get("lang_code") or value.pop("lang_code", None) 47 | } 48 | api_info = {k: v for k, v in api_info.items() if v is not None} 49 | cfg_copy[key]['api'] = api_info 50 | if cfg_copy != config: 51 | await write_config_file(cfg_copy, config_path) 52 | 53 | def import_session_json(session_path: str) -> dict: 54 | lang_pack = { 55 | 6: "android", 56 | 4: "android", 57 | 2040: 'tdesktop', 58 | 10840: 'ios', 59 | 21724: "android", 60 | } 61 | json_path = f"{session_path.replace('.session', '')}.json" 62 | if path.isfile(json_path): 63 | with open(json_path, 'r') as file: 64 | json_conf = json.loads(file.read()) 65 | api = { 66 | 'api_id': int(json_conf.get('app_id')), 67 | 'api_hash': json_conf.get('app_hash'), 68 | 'device_model': json_conf.get('device'), 69 | 'system_version': json_conf.get('sdk'), 70 | 'app_version': json_conf.get('app_version'), 71 | 'system_lang_code': json_conf.get('system_lang_code'), 72 | 'lang_code': json_conf.get('lang_code'), 73 | 'lang_pack': json_conf.get('lang_pack', lang_pack[int(json_conf.get('app_id'))]) 74 | } 75 | remove(json_path) 76 | return api 77 | return None 78 | 79 | def get_api(acc_api: dict) -> API: 80 | api_generators = { 81 | 4: API.TelegramAndroid.Generate, 82 | 6: API.TelegramAndroid.Generate, 83 | 2040: API.TelegramDesktop.Generate, 84 | 10840: API.TelegramIOS.Generate, 85 | 21724: API.TelegramAndroidX.Generate 86 | } 87 | generate_api = api_generators.get(acc_api.get('api_id'), API.TelegramDesktop.Generate) 88 | api = generate_api() 89 | api.api_id = acc_api.get('api_id', api.api_id) 90 | api.api_hash = acc_api.get('api_hash', api.api_hash) 91 | api.device_model = acc_api.get('device_model', api.device_model) 92 | api.system_version = acc_api.get('system_version', api.system_version) 93 | api.app_version = acc_api.get('app_version', api.app_version) 94 | api.system_lang_code = acc_api.get('system_lang_code', api.system_lang_code) 95 | api.lang_code = acc_api.get('lang_code', api.lang_code) 96 | api.lang_pack = acc_api.get('lang_pack', api.lang_pack) 97 | return api -------------------------------------------------------------------------------- /bot/utils/updater.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import asyncio 4 | import subprocess 5 | from typing import Optional 6 | from bot.utils import logger 7 | from bot.config import settings 8 | 9 | class UpdateManager: 10 | def __init__(self): 11 | self.branch = "main" 12 | self.check_interval = settings.CHECK_UPDATE_INTERVAL 13 | self.is_update_restart = "--update-restart" in sys.argv 14 | self._configure_git_safe_directory() 15 | self._ensure_uv_installed() 16 | 17 | def _configure_git_safe_directory(self) -> None: 18 | try: 19 | current_dir = os.getcwd() 20 | subprocess.run( 21 | ["git", "config", "--global", "--add", "safe.directory", current_dir], 22 | check=True, 23 | capture_output=True 24 | ) 25 | logger.info("Git safe.directory configured successfully") 26 | except subprocess.CalledProcessError as e: 27 | logger.error(f"Failed to configure git safe.directory: {e}") 28 | 29 | def _ensure_uv_installed(self) -> None: 30 | try: 31 | subprocess.run(["uv", "--version"], check=True, capture_output=True) 32 | logger.info("uv package manager is already installed") 33 | except (subprocess.CalledProcessError, FileNotFoundError): 34 | logger.info("Installing uv package manager...") 35 | try: 36 | curl_process = subprocess.run( 37 | ["curl", "-LsSf", "https://astral.sh/uv/install.sh"], 38 | check=True, 39 | capture_output=True, 40 | text=True 41 | ) 42 | 43 | install_script_path = "/tmp/uv_install.sh" 44 | with open(install_script_path, "w") as f: 45 | f.write(curl_process.stdout) 46 | 47 | os.chmod(install_script_path, 0o755) 48 | subprocess.run([install_script_path], check=True) 49 | 50 | os.remove(install_script_path) 51 | 52 | logger.info("Successfully installed uv package manager") 53 | 54 | os.environ["PATH"] = f"{os.path.expanduser('~/.cargo/bin')}:{os.environ.get('PATH', '')}" 55 | except subprocess.CalledProcessError as e: 56 | logger.error(f"Failed to install uv: {e}") 57 | sys.exit(1) 58 | except Exception as e: 59 | logger.error(f"Unexpected error while installing uv: {e}") 60 | sys.exit(1) 61 | 62 | def _check_dependency_files_changed(self) -> bool: 63 | try: 64 | result = subprocess.run( 65 | ["git", "diff", "--name-only", "HEAD@{1}", "HEAD"], 66 | capture_output=True, 67 | text=True, 68 | check=True 69 | ) 70 | changed_files = result.stdout.strip().split('\n') 71 | dependency_files = { 72 | "requirements.txt", 73 | "uv.lock", 74 | "pyproject.toml" 75 | } 76 | return any(file in changed_files for file in dependency_files) 77 | except subprocess.CalledProcessError as e: 78 | logger.error(f"Error checking dependency file changes: {e}") 79 | return True 80 | 81 | async def check_for_updates(self) -> bool: 82 | try: 83 | subprocess.run(["git", "fetch"], check=True, capture_output=True) 84 | result = subprocess.run( 85 | ["git", "status", "-uno"], 86 | capture_output=True, 87 | text=True, 88 | check=True 89 | ) 90 | return "Your branch is behind" in result.stdout 91 | except subprocess.CalledProcessError as e: 92 | logger.error(f"Error checking updates: {e}") 93 | return False 94 | 95 | def _pull_updates(self) -> bool: 96 | try: 97 | subprocess.run(["git", "pull"], check=True, capture_output=True) 98 | return True 99 | except subprocess.CalledProcessError as e: 100 | logger.error(f"Error updating: {e}") 101 | if e.stderr: 102 | logger.error(f"Git error details: {e.stderr.decode()}") 103 | return False 104 | 105 | def _install_dependencies(self) -> bool: 106 | if not self._check_dependency_files_changed(): 107 | logger.info("📦 No changes in dependency files, skipping installation") 108 | return True 109 | 110 | logger.info("📦 Changes detected in dependency files, updating dependencies...") 111 | 112 | try: 113 | if os.path.exists("pyproject.toml"): 114 | logger.info("Installing dependencies from pyproject.toml...") 115 | if os.path.exists("uv.lock"): 116 | subprocess.run(["uv", "pip", "sync"], check=True) 117 | else: 118 | subprocess.run(["uv", "pip", "install", "."], check=True) 119 | elif os.path.exists("uv.lock"): 120 | logger.info("Installing dependencies from uv.lock...") 121 | subprocess.run(["uv", "pip", "sync"], check=True) 122 | elif os.path.exists("requirements.txt"): 123 | logger.info("Installing dependencies from requirements.txt...") 124 | subprocess.run(["uv", "pip", "install", "-r", "requirements.txt"], check=True) 125 | else: 126 | logger.warning("No dependency files found") 127 | return False 128 | return True 129 | except subprocess.CalledProcessError as e: 130 | logger.error(f"Error installing dependencies: {e}") 131 | return False 132 | 133 | async def update_and_restart(self) -> None: 134 | logger.info("🔄 Update detected! Starting update process...") 135 | 136 | if not self._pull_updates(): 137 | logger.error("❌ Failed to pull updates") 138 | return 139 | 140 | if not self._install_dependencies(): 141 | logger.error("❌ Failed to update dependencies") 142 | return 143 | 144 | logger.info("✅ Update successfully installed! Restarting application...") 145 | 146 | new_args = [sys.executable, sys.argv[0], "-a", "1", "--update-restart"] 147 | os.execv(sys.executable, new_args) 148 | 149 | async def run(self) -> None: 150 | if not self.is_update_restart: 151 | await asyncio.sleep(10) 152 | 153 | while True: 154 | try: 155 | if await self.check_for_updates(): 156 | await self.update_and_restart() 157 | await asyncio.sleep(self.check_interval) 158 | except Exception as e: 159 | logger.error(f"Error during update check: {e}") 160 | await asyncio.sleep(60) 161 | 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖 Qlyuker Bot 2 | 3 | > **Automated Telegram Bot for Task Management and Bonus Collection** 4 | 5 | [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/downloads/) 6 | [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) 7 | [![Telegram](https://img.shields.io/badge/Telegram-Bot-blue.svg)](https://telegram.org/) 8 | 9 | [🇷🇺 Русский](README_RU.md) | [🇬🇧 English](README.md) 10 | 11 | [Market Link](https://t.me/MaineMarketBot?start=8HVF7S9K) 12 | [Channel Link](https://t.me/+vpXdTJ_S3mo0ZjIy) 13 | [Chat Link](https://t.me/+wWQuct9bljQ0ZDA6) 14 | 15 | --- 16 | 17 | ## 💻 About the Software 18 | 19 | **Qlyuker Bot** is a professional automation solution built with Python for Telegram bot interaction. The software leverages modern asynchronous programming patterns and provides enterprise-grade features for managing multiple accounts efficiently. 20 | 21 | ### 🔧 Technical Stack 22 | - **Language**: Python 3.10+ 23 | - **Framework**: Pyrogram (Telegram MTProto API) 24 | - **Architecture**: Asynchronous multi-threaded 25 | - **Deployment**: Docker & Docker Compose support 26 | - **Package Manager**: UV (modern Python package manager) 27 | 28 | ### 🎯 Use Cases 29 | - Automated task completion in Telegram bots 30 | - Multi-account management with proxy support 31 | - Daily bonus collection automation 32 | - Referral program participation 33 | - Session management via QR codes 34 | 35 | --- 36 | 37 | ## 📑 Table of Contents 38 | 1. [Description](#description) 39 | 2. [Key Features](#key-features) 40 | 3. [Installation](#installation) 41 | - [Quick Start](#quick-start) 42 | - [Manual Installation](#manual-installation) 43 | 4. [Settings](#settings) 44 | 5. [Support and Donations](#support-and-donations) 45 | 6. [Contact](#contact) 46 | 47 | --- 48 | 49 | ## 📜 Description 50 | **Qlyuker Bot** is a powerful bot for Telegram that helps automate interaction with the bot. It supports multithreading, proxy integration, and session creation via QR codes. 51 | 52 | --- 53 | 54 | ## 🌟 Key Features 55 | - 🔄 **Multithreading** — supports parallel processes to increase work speed. 56 | - 🔐 **Proxy binding to session** — allows secure work through proxy servers. 57 | - 📲 **Auto-account registration** — quick account registration via referral links. 58 | - 🎁 **Bonus automation** — automatic collection of daily bonuses without manual actions. 59 | - 📸 **Session creation via QR code** — fast and convenient session generation through a mobile app. 60 | - 📄 **Support for pyrogram session format (.session)** — easy integration with the Telegram API for session storage. 61 | 62 | --- 63 | 64 | ## 🛠️ Installation 65 | 66 | ### Quick Start 67 | 1. **Download the project:** 68 | ```bash 69 | git clone https://bitbucket.org:mffff4/qlyukerbot.git 70 | cd qlyukerbot 71 | ``` 72 | 73 | 2. **Install dependencies:** 74 | - **Windows**: 75 | ```bash 76 | run.bat 77 | ``` 78 | - **Linux**: 79 | ```bash 80 | run.sh 81 | ``` 82 | 83 | 3. **Obtain API keys:** 84 | - Go to [my.telegram.org](https://my.telegram.org) and get your `API_ID` and `API_HASH`. 85 | - Add this information to the `.env` file. 86 | 87 | 4. **Run the bot:** 88 | ```bash 89 | python3 main.py --a 1 # Run the bot 90 | ``` 91 | 92 | ### Manual Installation 93 | 1. **Linux:** 94 | ```bash 95 | sudo sh install.sh 96 | python3 -m venv venv 97 | source venv/bin/activate 98 | pip3 install -r requirements.txt 99 | cp .env-example .env 100 | nano .env # Add your API_ID and API_HASH 101 | python3 main.py 102 | ``` 103 | 104 | 2. **Windows:** 105 | ```bash 106 | python -m venv venv 107 | venv\Scripts\activate 108 | pip install -r requirements.txt 109 | copy .env-example .env 110 | python main.py 111 | ``` 112 | --- 113 | 114 | ## ⚙️ Settings 115 | 116 | | Parameter | Default Value | Description | 117 | |---------------------------|----------------------|-------------------------------------------------------------| 118 | | **API_ID** | | Telegram API application ID | 119 | | **API_HASH** | | Telegram API application hash | 120 | | **GLOBAL_CONFIG_PATH** | | Path for configuration files. By default, uses the TG_FARM environment variable | 121 | | **FIX_CERT** | False | Fix SSL certificate errors | 122 | | **SESSION_START_DELAY** | 360 | Delay before starting the session (seconds) | 123 | | **REF_ID** | | Referral ID for new accounts | 124 | | **USE_PROXY** | True | Use proxy | 125 | | **SESSIONS_PER_PROXY** | 1 | Number of sessions per proxy | 126 | | **DISABLE_PROXY_REPLACE** | False | Disable proxy replacement on errors | 127 | | **BLACKLISTED_SESSIONS** | "" | Sessions that will not be used (comma-separated) | 128 | | **DEBUG_LOGGING** | False | Enable detailed logging | 129 | | **DEVICE_PARAMS** | False | Use custom device parameters | 130 | | **AUTO_UPDATE** | True | Automatic updates | 131 | | **CHECK_UPDATE_INTERVAL** | 300 | Update check interval (seconds) | 132 | 133 | 134 | ## 💰 Support and Donations 135 | 136 | Support development using cryptocurrencies: 137 | 138 | | Currency | Wallet Address | 139 | |----------------------|------------------------------------------------------------------------------------| 140 | | Bitcoin (BTC) |bc1qt84nyhuzcnkh2qpva93jdqa20hp49edcl94nf6| 141 | | Ethereum (ETH) |0xc935e81045CAbE0B8380A284Ed93060dA212fa83| 142 | | TON |UQBlvCgM84ijBQn0-PVP3On0fFVWds5SOHilxbe33EDQgryz| 143 | | Binance Coin |0xc935e81045CAbE0B8380A284Ed93060dA212fa83| 144 | | Solana (SOL) |3vVxkGKasJWCgoamdJiRPy6is4di72xR98CDj2UdS1BE| 145 | | Ripple (XRP) |rPJzfBcU6B8SYU5M8h36zuPcLCgRcpKNB4| 146 | | Dogecoin (DOGE) |DST5W1c4FFzHVhruVsa2zE6jh5dznLDkmW| 147 | | Polkadot (DOT) |1US84xhUghAhrMtw2bcZh9CXN3i7T1VJB2Gdjy9hNjR3K71| 148 | | Litecoin (LTC) |ltc1qcg8qesg8j4wvk9m7e74pm7aanl34y7q9rutvwu| 149 | | Matic |0xc935e81045CAbE0B8380A284Ed93060dA212fa83| 150 | | Tron (TRX) |TQkDWCjchCLhNsGwr4YocUHEeezsB4jVo5| 151 | 152 | --- 153 | 154 | ## 📞 Contact 155 | 156 | If you have questions or suggestions: 157 | - **Telegram**: [Join our channel](https://t.me/+vpXdTJ_S3mo0ZjIy) 158 | 159 | --- 160 | 161 | ## ⚠️ Disclaimer 162 | 163 | This software is provided "as is" without any warranties. By using this bot, you accept full responsibility for its use and any consequences that may arise. 164 | 165 | The author is not responsible for: 166 | - Any direct or indirect damages related to the use of the bot 167 | - Possible violations of third-party service terms of use 168 | - Account blocking or access restrictions 169 | 170 | Use the bot at your own risk and in compliance with applicable laws and third-party service terms of use. 171 | 172 | -------------------------------------------------------------------------------- /README_RU.md: -------------------------------------------------------------------------------- 1 | # 🤖 Qlyuker Bot 2 | 3 | > **Автоматизированный Telegram-бот для управления задачами и сбора бонусов** 4 | 5 | [![Python](https://img.shields.io/badge/Python-3.10+-blue.svg)](https://www.python.org/downloads/) 6 | [![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) 7 | [![Telegram](https://img.shields.io/badge/Telegram-Bot-blue.svg)](https://telegram.org/) 8 | 9 | [🇷🇺 Русский](README_RU.md) | [🇬🇧 English](README.md) 10 | 11 | [Market Link](https://t.me/MaineMarketBot?start=8HVF7S9K) 12 | [Channel Link](https://t.me/+vpXdTJ_S3mo0ZjIy) 13 | [Chat Link](https://t.me/+wWQuct9bljQ0ZDA6) 14 | 15 | --- 16 | 17 | ## 💻 О программном обеспечении 18 | 19 | **Qlyuker Bot** — это профессиональное решение для автоматизации, разработанное на Python для взаимодействия с Telegram-ботами. Программа использует современные паттерны асинхронного программирования и предоставляет функции корпоративного уровня для эффективного управления несколькими аккаунтами. 20 | 21 | ### 🔧 Технологический стек 22 | - **Язык**: Python 3.10+ 23 | - **Фреймворк**: Pyrogram (Telegram MTProto API) 24 | - **Архитектура**: Асинхронная многопоточная 25 | - **Развертывание**: Поддержка Docker и Docker Compose 26 | - **Менеджер пакетов**: UV (современный менеджер пакетов Python) 27 | 28 | ### 🎯 Сценарии использования 29 | - Автоматическое выполнение задач в Telegram-ботах 30 | - Управление несколькими аккаунтами с поддержкой прокси 31 | - Автоматизация сбора ежедневных бонусов 32 | - Участие в реферальных программах 33 | - Управление сессиями через QR-коды 34 | 35 | --- 36 | 37 | ## 📑 Оглавление 38 | 1. [Описание](#описание) 39 | 2. [Ключевые особенности](#ключевые-особенности) 40 | 3. [Установка](#установка) 41 | - [Быстрый старт](#быстрый-старт) 42 | - [Ручная установка](#ручная-установка) 43 | 4. [Настройки](#настройки) 44 | 5. [Поддержка и донаты](#поддержка-и-донаты) 45 | 6. [Контакты](#контакты) 46 | 47 | --- 48 | 49 | ## 📜 Описание 50 | **Qlyuker Bot** — это мощный бот для Telegram, который помогает автоматизировать взаимодействие с ботом. Поддерживает многопоточность, работу с прокси и создание сессий через QR-коды. 51 | 52 | --- 53 | 54 | ## 🌟 Ключевые особенности 55 | - 🔄 **Многопоточность** — поддержка параллельных процессов для повышения скорости работы. 56 | - 🔐 **Привязка прокси к сессии** — возможность безопасной работы через прокси-сервера. 57 | - 📲 **Авто-регистрация аккаунтов** — быстрая регистрация аккаунтов по реферальным ссылкам. 58 | - 🎁 **Автоматизация бонусов** — сбор ежедневных бонусов без необходимости ручных действий. 59 | - 📸 **Создание сессий через QR-код** — быстрая и удобная генерация сессий через мобильное приложение. 60 | - 📄 **Поддержка формата сессий pyrogram (.session)** — простая интеграция с API Telegram для хранения сессий. 61 | 62 | --- 63 | 64 | ## 🛠️ Установка 65 | 66 | ### Быстрый старт 67 | 1. **Скачайте проект:** 68 | ```bash 69 | git clone https://bitbucket.org:mffff4/qlyukerbot.git 70 | cd qlyukerbot 71 | ``` 72 | 73 | 2. **Установите зависимости:** 74 | - **Windows**: 75 | ```bash 76 | run.bat 77 | ``` 78 | - **Linux**: 79 | ```bash 80 | run.sh 81 | ``` 82 | 83 | 3. **Получите API ключи:** 84 | - Перейдите на [my.telegram.org](https://my.telegram.org) и получите `API_ID` и `API_HASH`. 85 | - Добавьте эти данные в файл `.env`. 86 | 87 | 4. **Запустите бота:** 88 | ```bash 89 | python3 main.py --a 1 # Запустить бота 90 | ``` 91 | 92 | ### Ручная установка 93 | 1. **Linux:** 94 | ```bash 95 | sudo sh install.sh 96 | python3 -m venv venv 97 | source venv/bin/activate 98 | pip3 install -r requirements.txt 99 | cp .env-example .env 100 | nano .env # Укажите свои API_ID и API_HASH 101 | python3 main.py 102 | ``` 103 | 104 | 2. **Windows:** 105 | ```bash 106 | python -m venv venv 107 | venv\Scripts\activate 108 | pip install -r requirements.txt 109 | copy .env-example .env 110 | python main.py 111 | ``` 112 | 113 | --- 114 | 115 | 116 | ## ⚙️ Настройки 117 | 118 | | Параметр | Значение по умолчанию | Описание | 119 | |---------------------------|----------------------|---------------------------------------------------------| 120 | | **API_ID** | | Идентификатор приложения Telegram API | 121 | | **API_HASH** | | Хэш приложения Telegram API | 122 | | **GLOBAL_CONFIG_PATH** | | Путь к файлам конфигурации. По умолчанию используется переменная окружения TG_FARM | 123 | | **FIX_CERT** | False | Исправить ошибки сертификата SSL | 124 | | **SESSION_START_DELAY** | 360 | Задержка перед началом сессии (в секундах) | 125 | | **REF_ID** | | Идентификатор реферала для новых аккаунтов | 126 | | **USE_PROXY** | True | Использовать прокси | 127 | | **SESSIONS_PER_PROXY** | 1 | Количество сессий на один прокси | 128 | | **DISABLE_PROXY_REPLACE** | False | Отключить замену прокси при ошибках | 129 | | **BLACKLISTED_SESSIONS** | "" | Сессии, которые не будут использоваться (через запятую)| 130 | | **DEBUG_LOGGING** | False | Включить подробный логгинг | 131 | | **DEVICE_PARAMS** | False | Использовать пользовательские параметры устройства | 132 | | **AUTO_UPDATE** | True | Автоматические обновления | 133 | | **CHECK_UPDATE_INTERVAL** | 300 | Интервал проверки обновлений (в секундах) | 134 | 135 | --- 136 | 137 | ## 💰 Поддержка и донаты 138 | 139 | Поддержите разработку с помощью криптовалют или платформ: 140 | 141 | | Валюта | Адрес кошелька | 142 | |----------------------|-------------------------------------------------------------------------------------| 143 | | Bitcoin (BTC) |bc1qt84nyhuzcnkh2qpva93jdqa20hp49edcl94nf6| 144 | | Ethereum (ETH) |0xc935e81045CAbE0B8380A284Ed93060dA212fa83| 145 | | TON |UQBlvCgM84ijBQn0-PVP3On0fFVWds5SOHilxbe33EDQgryz| 146 | | Binance Coin |0xc935e81045CAbE0B8380A284Ed93060dA212fa83| 147 | | Solana (SOL) |3vVxkGKasJWCgoamdJiRPy6is4di72xR98CDj2UdS1BE| 148 | | Ripple (XRP) |rPJzfBcU6B8SYU5M8h36zuPcLCgRcpKNB4| 149 | | Dogecoin (DOGE) |DST5W1c4FFzHVhruVsa2zE6jh5dznLDkmW| 150 | | Polkadot (DOT) |1US84xhUghAhrMtw2bcZh9CXN3i7T1VJB2Gdjy9hNjR3K71| 151 | | Litecoin (LTC) |ltc1qcg8qesg8j4wvk9m7e74pm7aanl34y7q9rutvwu| 152 | | Matic |0xc935e81045CAbE0B8380A284Ed93060dA212fa83| 153 | | Tron (TRX) |TQkDWCjchCLhNsGwr4YocUHEeezsB4jVo5| 154 | 155 | --- 156 | 157 | ## 📞 Контакты 158 | 159 | Если у вас возникли вопросы или предложения: 160 | - **Telegram**: [Присоединяйтесь к нашему каналу](https://t.me/+vpXdTJ_S3mo0ZjIy) 161 | 162 | --- 163 | ## ⚠️ Дисклеймер 164 | 165 | Данное программное обеспечение предоставляется "как есть", без каких-либо гарантий. Используя этот бот, вы принимаете на себя полную ответственность за его использование и любые последствия, которые могут возникнуть. 166 | 167 | Автор не несет ответственности за: 168 | - Любой прямой или косвенный ущерб, связанный с использованием бота 169 | - Возможные нарушения условий использования сторонних сервисов 170 | - Блокировку или ограничение доступа к аккаунтам 171 | 172 | Используйте бота на свой страх и риск и в соответствии с применимым законодательством и условиями использования сторонних сервисов. 173 | -------------------------------------------------------------------------------- /bot/utils/ad_viewer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import datetime, timezone 3 | from typing import Dict, Optional, List, Any, Callable, Union, TypeVar 4 | from abc import ABC, abstractmethod 5 | import asyncio 6 | import aiohttp 7 | from random import uniform 8 | from urllib.parse import urlencode 9 | import json 10 | 11 | from bot.utils.logger import logger, log_error 12 | from bot.exceptions import AdViewError 13 | 14 | T = TypeVar('T') 15 | 16 | @dataclass 17 | class AdEventConfig: 18 | event_type: str 19 | tracking_type_id: str 20 | min_delay: float = 0.0 21 | max_delay: float = 0.0 22 | required: bool = True 23 | retry_count: int = 1 24 | 25 | @dataclass 26 | class AdConfig: 27 | 28 | min_view_duration: float = 15.0 29 | max_view_duration: float = 20.0 30 | min_delay_between_ads: float = 2.0 31 | max_delay_between_ads: float = 5.0 32 | max_retries: int = 3 33 | retry_delay: float = 5.0 34 | 35 | 36 | user_agent: str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" 37 | platform: str = "MacIntel" 38 | language: str = "ru" 39 | connection_type: str = "1" 40 | device_platform: str = "android" 41 | 42 | 43 | events: List[AdEventConfig] = field(default_factory=lambda: [ 44 | AdEventConfig("render", "13", 0.0, 0.5), 45 | AdEventConfig("show", "0", 1.0, 2.0), 46 | AdEventConfig("reward", "14", 0.0, 0.5, True, 3) 47 | ]) 48 | 49 | 50 | additional_params: Dict[str, str] = field(default_factory=dict) 51 | 52 | 53 | proxy_url: Optional[str] = None 54 | proxy_auth: Optional[Dict[str, str]] = None 55 | 56 | class AdEventHandler(ABC): 57 | 58 | @abstractmethod 59 | async def on_ad_start(self, ad_data: Dict[str, Any]) -> None: 60 | pass 61 | 62 | @abstractmethod 63 | async def on_ad_complete(self, ad_data: Dict[str, Any], success: bool) -> None: 64 | pass 65 | 66 | @abstractmethod 67 | async def on_ad_error(self, error: Exception, attempt: int) -> None: 68 | pass 69 | 70 | class DefaultAdEventHandler(AdEventHandler): 71 | 72 | async def on_ad_start(self, ad_data: Dict[str, Any]) -> None: 73 | logger.info("Ad viewing started") 74 | 75 | async def on_ad_complete(self, ad_data: Dict[str, Any], success: bool) -> None: 76 | status = "successfully" if success else "unsuccessfully" 77 | logger.info(f"Ad viewing completed {status}") 78 | 79 | async def on_ad_error(self, error: Exception, attempt: int) -> None: 80 | log_error(f"Error during ad viewing (attempt {attempt}): {str(error)}") 81 | 82 | class AdViewer: 83 | 84 | def __init__( 85 | self, 86 | base_url: str, 87 | event_url: str, 88 | block_id: str, 89 | http_client: aiohttp.ClientSession, 90 | access_token: str, 91 | user_id: Union[int, str], 92 | config: Optional[AdConfig] = None, 93 | event_handler: Optional[AdEventHandler] = None, 94 | custom_headers: Optional[Dict[str, str]] = None 95 | ) -> None: 96 | self._base_url = base_url 97 | self._event_url = event_url 98 | self._block_id = block_id 99 | self._http_client = http_client 100 | self._access_token = access_token 101 | self._user_id = str(user_id) 102 | self._config = config or AdConfig() 103 | self._event_handler = event_handler or DefaultAdEventHandler() 104 | self._custom_headers = custom_headers or {} 105 | 106 | 107 | self._validate_config() 108 | 109 | def _validate_config(self) -> None: 110 | if self._config.min_view_duration > self._config.max_view_duration: 111 | raise ValueError("min_view_duration cannot be greater than max_view_duration") 112 | if self._config.min_delay_between_ads > self._config.max_delay_between_ads: 113 | raise ValueError("min_delay_between_ads cannot be greater than max_delay_between_ads") 114 | 115 | def _get_base_params(self) -> Dict[str, str]: 116 | params = { 117 | "blockId": self._block_id, 118 | "tg_id": self._user_id, 119 | "tg_platform": self._config.device_platform, 120 | "platform": self._config.platform, 121 | "language": self._config.language, 122 | "top_domain": self._base_url.split('/')[2], 123 | "connectiontype": self._config.connection_type, 124 | **self._config.additional_params 125 | } 126 | return {k: v for k, v in params.items() if v is not None} 127 | 128 | def _get_headers(self, additional_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]: 129 | headers = { 130 | "Authorization": f"Bearer {self._access_token}", 131 | "User-Agent": self._config.user_agent, 132 | "Accept": "application/json", 133 | "Accept-Language": self._config.language, 134 | **self._custom_headers 135 | } 136 | if additional_headers: 137 | headers.update(additional_headers) 138 | return headers 139 | 140 | async def _make_request( 141 | self, 142 | url: str, 143 | method: str = "GET", 144 | params: Optional[Dict[str, str]] = None, 145 | data: Optional[Dict[str, Any]] = None, 146 | headers: Optional[Dict[str, str]] = None, 147 | timeout: Optional[float] = None 148 | ) -> Dict[str, Any]: 149 | try: 150 | request_kwargs = { 151 | "method": method, 152 | "url": url, 153 | "headers": self._get_headers(headers), 154 | "timeout": aiohttp.ClientTimeout(total=timeout) if timeout else None 155 | } 156 | 157 | if params: 158 | request_kwargs["params"] = params 159 | if data: 160 | request_kwargs["json"] = data 161 | if self._config.proxy_url: 162 | request_kwargs["proxy"] = self._config.proxy_url 163 | if self._config.proxy_auth: 164 | request_kwargs["proxy_auth"] = aiohttp.BasicAuth(**self._config.proxy_auth) 165 | 166 | async with self._http_client.request(**request_kwargs) as response: 167 | if response.status != 200: 168 | error_text = await response.text() 169 | raise AdViewError( 170 | f"Request error {response.status}: {error_text}" 171 | ) 172 | return await response.json() 173 | 174 | except asyncio.TimeoutError: 175 | raise AdViewError("Request timeout exceeded") 176 | except Exception as e: 177 | raise AdViewError(f"Error during request execution: {str(e)}") 178 | 179 | async def _get_ad(self) -> Dict[str, Any]: 180 | params = { 181 | **self._get_base_params(), 182 | "request_id": str(int(datetime.now(timezone.utc).timestamp() * 1000)) 183 | } 184 | return await self._make_request(self._base_url, params=params) 185 | 186 | async def _process_ad_event( 187 | self, 188 | event_config: AdEventConfig, 189 | tracking_data: Dict[str, str] 190 | ) -> bool: 191 | record = tracking_data.get(event_config.event_type) 192 | if not record: 193 | if event_config.required: 194 | raise AdViewError(f"Missing required event: {event_config.event_type}") 195 | return True 196 | 197 | for attempt in range(event_config.retry_count): 198 | try: 199 | params = { 200 | "record": record, 201 | "type": event_config.event_type, 202 | "trackingtypeid": event_config.tracking_type_id 203 | } 204 | await self._make_request(self._event_url, params=params) 205 | 206 | if event_config.max_delay > 0: 207 | await asyncio.sleep( 208 | uniform(event_config.min_delay, event_config.max_delay) 209 | ) 210 | return True 211 | 212 | except AdViewError as e: 213 | if attempt == event_config.retry_count - 1: 214 | if event_config.required: 215 | raise 216 | return False 217 | await asyncio.sleep(self._config.retry_delay) 218 | 219 | return False 220 | 221 | async def _simulate_ad_view(self, tracking_data: Dict[str, str]) -> bool: 222 | try: 223 | for event_config in self._config.events: 224 | if not await self._process_ad_event(event_config, tracking_data): 225 | return False 226 | 227 | view_duration = uniform( 228 | self._config.min_view_duration, 229 | self._config.max_view_duration 230 | ) 231 | logger.info(f"Ad viewed for {view_duration:.1f} seconds") 232 | await asyncio.sleep(view_duration) 233 | 234 | return True 235 | 236 | except Exception as e: 237 | log_error(f"Error during ad viewing: {str(e)}") 238 | return False 239 | 240 | def _extract_tracking_data(self, ad_data: Dict[str, Any]) -> Dict[str, str]: 241 | try: 242 | tracking_list = ad_data.get("banner", {}).get("trackings", []) 243 | return { 244 | tracking["name"]: tracking["value"] 245 | for tracking in tracking_list 246 | if "name" in tracking and "value" in tracking 247 | } 248 | except Exception as e: 249 | raise AdViewError(f"Invalid ad data format: {str(e)}") 250 | 251 | async def view_ads( 252 | self, 253 | count: int, 254 | success_callback: Optional[Callable[[Dict[str, Any]], Any]] = None 255 | ) -> int: 256 | successful_views = 0 257 | 258 | for i in range(count): 259 | logger.info(f"Starting ad viewing {i + 1}/{count}") 260 | 261 | for attempt in range(self._config.max_retries): 262 | try: 263 | 264 | ad_data = await self._get_ad() 265 | await self._event_handler.on_ad_start(ad_data) 266 | 267 | tracking_data = self._extract_tracking_data(ad_data) 268 | success = await self._simulate_ad_view(tracking_data) 269 | 270 | await self._event_handler.on_ad_complete(ad_data, success) 271 | 272 | if success: 273 | successful_views += 1 274 | if success_callback: 275 | await success_callback(ad_data) 276 | logger.info(f"Successfully viewed ad {i + 1}/{count}") 277 | break 278 | 279 | except Exception as e: 280 | await self._event_handler.on_ad_error(e, attempt + 1) 281 | if attempt < self._config.max_retries - 1: 282 | await asyncio.sleep(self._config.retry_delay) 283 | continue 284 | 285 | 286 | if i < count - 1: 287 | delay = uniform( 288 | self._config.min_delay_between_ads, 289 | self._config.max_delay_between_ads 290 | ) 291 | await asyncio.sleep(delay) 292 | 293 | return successful_views -------------------------------------------------------------------------------- /bot/core/launcher.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import asyncio 3 | import argparse 4 | import os 5 | import subprocess 6 | import signal 7 | from copy import deepcopy 8 | from random import uniform 9 | from colorama import init, Fore, Style 10 | import shutil 11 | from typing import Optional 12 | 13 | from bot.utils.universal_telegram_client import UniversalTelegramClient 14 | from bot.utils.web import run_web_and_tunnel, stop_web_and_tunnel 15 | from bot.config import settings 16 | from bot.core.agents import generate_random_user_agent 17 | from bot.utils import logger, config_utils, proxy_utils, CONFIG_PATH, SESSIONS_PATH, PROXIES_PATH 18 | from bot.core.tapper import run_tapper 19 | from bot.core.registrator import register_sessions 20 | from bot.utils.updater import UpdateManager 21 | from bot.exceptions import InvalidSession 22 | 23 | from telethon.errors import ( 24 | AuthKeyUnregisteredError, AuthKeyDuplicatedError, AuthKeyError, 25 | SessionPasswordNeededError 26 | ) 27 | 28 | from pyrogram.errors import ( 29 | AuthKeyUnregistered as PyrogramAuthKeyUnregisteredError, 30 | SessionPasswordNeeded as PyrogramSessionPasswordNeededError, 31 | SessionRevoked as PyrogramSessionRevoked 32 | ) 33 | 34 | init() 35 | shutdown_event = asyncio.Event() 36 | 37 | def signal_handler(signum: int, frame) -> None: 38 | shutdown_event.set() 39 | 40 | START_TEXT = f""" 41 | {Fore.RED}ВНИМАНИЕ: Эта ферма не предназначена для продажи!{Style.RESET_ALL} 42 | {Fore.RED}WARNING: This farm is not for sale!{Style.RESET_ALL} 43 | {Fore.RED}¡ADVERTENCIA: ¡Esta granja no está a la venta!{Style.RESET_ALL} 44 | {Fore.RED}ATTENTION: Cette ferme n'est pas à vendre!{Style.RESET_ALL} 45 | {Fore.RED}ACHTUNG: Diese Farm ist nicht zum Verkauf bestimmt!{Style.RESET_ALL} 46 | {Fore.RED}ATTENZIONE: Questa fattoria non è in vendita!{Style.RESET_ALL} 47 | {Fore.RED}注意:この農場は販売用ではありません!{Style.RESET_ALL} 48 | {Fore.RED}주의: 이 농장은 판매용이 아닙니다!{Style.RESET_ALL} 49 | {Fore.RED}注意:此农场不用于销售!{Style.RESET_ALL} 50 | {Fore.RED}ATENÇÃO: Esta fazenda não se destina à venda!{Style.RESET_ALL} 51 | 52 | {Fore.LIGHTMAGENTA_EX} 53 | LOGO 54 | {Style.RESET_ALL} 55 | {Fore.CYAN}Select action:{Style.RESET_ALL} 56 | 57 | {Fore.GREEN}1. Launch clicker{Style.RESET_ALL} 58 | {Fore.GREEN}2. Create session{Style.RESET_ALL} 59 | {Fore.GREEN}3. Create session via QR{Style.RESET_ALL} 60 | {Fore.GREEN}4. Upload sessions via web (BETA){Style.RESET_ALL} 61 | 62 | {Fore.CYAN}Developed by: @Mffff4{Style.RESET_ALL} 63 | {Fore.CYAN}Our Telegram channel: {Fore.BLUE}https://t.me/+x8gutImPtaQyN2Ey{Style.RESET_ALL} 64 | """ 65 | 66 | API_ID = settings.API_ID 67 | API_HASH = settings.API_HASH 68 | 69 | def prompt_user_action() -> int: 70 | logger.info(START_TEXT) 71 | while True: 72 | action = input("> ").strip() 73 | if action.isdigit() and action in ("1", "2", "3", "4"): 74 | return int(action) 75 | logger.warning("Invalid action. Please enter a number between 1 and 4.") 76 | 77 | async def process() -> None: 78 | parser = argparse.ArgumentParser() 79 | parser.add_argument("-a", "--action", type=int, help="Action to perform") 80 | parser.add_argument("--update-restart", action="store_true", help=argparse.SUPPRESS) 81 | args = parser.parse_args() 82 | 83 | if not settings.USE_PROXY: 84 | logger.info(f"Detected {len(get_sessions(SESSIONS_PATH))} sessions | USE_PROXY=False") 85 | else: 86 | logger.info(f"Detected {len(get_sessions(SESSIONS_PATH))} sessions | " 87 | f"{len(proxy_utils.get_proxies(PROXIES_PATH))} proxies") 88 | 89 | action = args.action 90 | if not action and not args.update_restart: 91 | action = prompt_user_action() 92 | 93 | if action == 1: 94 | if not API_ID or not API_HASH: 95 | raise ValueError("API_ID and API_HASH not found in the .env file.") 96 | await run_tasks() 97 | elif action == 2: 98 | await register_sessions() 99 | elif action == 3: 100 | session_name = input("Enter the session name for QR code authentication: ") 101 | print("Initializing QR code authentication...") 102 | subprocess.run(["python", "-m", "bot.utils.loginQR", "-s", session_name]) 103 | print("QR code authentication was successful!") 104 | elif action == 4: 105 | logger.info("Starting web interface for uploading sessions...") 106 | signal.signal(signal.SIGINT, signal_handler) 107 | try: 108 | web_task = asyncio.create_task(run_web_and_tunnel()) 109 | await shutdown_event.wait() 110 | finally: 111 | web_task.cancel() 112 | await stop_web_and_tunnel() 113 | print("Program terminated.") 114 | 115 | async def move_invalid_session_to_inactive_folder(session_name: str) -> None: 116 | inactive_dir = os.path.join(SESSIONS_PATH, "inactive") 117 | os.makedirs(inactive_dir, exist_ok=True) 118 | 119 | session_patterns = [ 120 | f"{SESSIONS_PATH}/{session_name}.session", 121 | f"{SESSIONS_PATH}/telethon/{session_name}.session", 122 | f"{SESSIONS_PATH}/pyrogram/{session_name}.session" 123 | ] 124 | 125 | found = False 126 | for pattern in session_patterns: 127 | matching_files = glob.glob(pattern) 128 | for session_file in matching_files: 129 | found = True 130 | if os.path.exists(session_file): 131 | relative_path = os.path.relpath(os.path.dirname(session_file), SESSIONS_PATH) 132 | if relative_path == ".": 133 | target_dir = inactive_dir 134 | else: 135 | target_dir = os.path.join(inactive_dir, relative_path) 136 | os.makedirs(target_dir, exist_ok=True) 137 | 138 | target_path = os.path.join(target_dir, os.path.basename(session_file)) 139 | try: 140 | shutil.move(session_file, target_path) 141 | logger.warning(f"Session {session_name} moved to {target_path} due to invalidity") 142 | except Exception as e: 143 | logger.error(f"Error moving session {session_name}: {e}") 144 | 145 | if not found: 146 | logger.error(f"Session {session_name} not found when attempting to move to inactive folder") 147 | 148 | def get_sessions(sessions_folder: str) -> list[str]: 149 | session_names = glob.glob(f"{sessions_folder}/*.session") 150 | session_names += glob.glob(f"{sessions_folder}/telethon/*.session") 151 | session_names += glob.glob(f"{sessions_folder}/pyrogram/*.session") 152 | return [file.replace('.session', '') for file in sorted(session_names)] 153 | 154 | async def get_tg_clients() -> list[UniversalTelegramClient]: 155 | session_paths = get_sessions(SESSIONS_PATH) 156 | 157 | if not session_paths: 158 | raise FileNotFoundError("Session files not found") 159 | tg_clients = [] 160 | for session in session_paths: 161 | session_name = os.path.basename(session) 162 | 163 | if session_name in settings.blacklisted_sessions: 164 | logger.warning(f"{session_name} | Session is blacklisted | Skipping") 165 | continue 166 | 167 | accounts_config = config_utils.read_config_file(CONFIG_PATH) 168 | session_config: dict = deepcopy(accounts_config.get(session_name, {})) 169 | if 'api' not in session_config: 170 | session_config['api'] = {} 171 | api_config = session_config.get('api', {}) 172 | api = None 173 | if api_config.get('api_id') in [4, 6, 2040, 10840, 21724]: 174 | api = config_utils.get_api(api_config) 175 | 176 | if api: 177 | client_params = { 178 | "session": session, 179 | "api": api 180 | } 181 | else: 182 | client_params = { 183 | "api_id": api_config.get("api_id", API_ID), 184 | "api_hash": api_config.get("api_hash", API_HASH), 185 | "session": session, 186 | "lang_code": api_config.get("lang_code", "en"), 187 | "system_lang_code": api_config.get("system_lang_code", "en-US") 188 | } 189 | 190 | for key in ("device_model", "system_version", "app_version"): 191 | if api_config.get(key): 192 | client_params[key] = api_config[key] 193 | 194 | session_config['user_agent'] = session_config.get('user_agent', generate_random_user_agent()) 195 | api_config.update(api_id=client_params.get('api_id') or client_params.get('api').api_id, 196 | api_hash=client_params.get('api_hash') or client_params.get('api').api_hash) 197 | 198 | session_proxy = session_config.get('proxy') 199 | if not session_proxy and 'proxy' in session_config.keys(): 200 | try: 201 | tg_clients.append(UniversalTelegramClient(**client_params)) 202 | if accounts_config.get(session_name) != session_config: 203 | await config_utils.update_session_config_in_file(session_name, session_config, CONFIG_PATH) 204 | except (AuthKeyUnregisteredError, AuthKeyDuplicatedError, AuthKeyError, 205 | SessionPasswordNeededError, PyrogramAuthKeyUnregisteredError, 206 | PyrogramSessionPasswordNeededError, 207 | PyrogramSessionRevoked, InvalidSession) as e: 208 | logger.error(f"{session_name} | Session initialization error: {e}") 209 | await move_invalid_session_to_inactive_folder(session_name) 210 | continue 211 | 212 | else: 213 | if settings.DISABLE_PROXY_REPLACE: 214 | proxy = session_proxy or next(iter(proxy_utils.get_unused_proxies(accounts_config, PROXIES_PATH)), None) 215 | else: 216 | proxy = await proxy_utils.get_working_proxy(accounts_config, session_proxy) \ 217 | if session_proxy or settings.USE_PROXY else None 218 | 219 | if not proxy and (settings.USE_PROXY or session_proxy): 220 | logger.warning(f"{session_name} | Didn't find a working unused proxy for session | Skipping") 221 | continue 222 | else: 223 | try: 224 | tg_clients.append(UniversalTelegramClient(**client_params)) 225 | session_config['proxy'] = proxy 226 | if accounts_config.get(session_name) != session_config: 227 | await config_utils.update_session_config_in_file(session_name, session_config, CONFIG_PATH) 228 | except (AuthKeyUnregisteredError, AuthKeyDuplicatedError, AuthKeyError, 229 | SessionPasswordNeededError, PyrogramAuthKeyUnregisteredError, 230 | PyrogramSessionPasswordNeededError, 231 | PyrogramSessionRevoked, InvalidSession) as e: 232 | logger.error(f"{session_name} | Session initialization error: {e}") 233 | await move_invalid_session_to_inactive_folder(session_name) 234 | 235 | return tg_clients 236 | 237 | async def init_config_file() -> None: 238 | session_paths = get_sessions(SESSIONS_PATH) 239 | 240 | if not session_paths: 241 | raise FileNotFoundError("Session files not found") 242 | for session in session_paths: 243 | session_name = os.path.basename(session) 244 | parsed_json = config_utils.import_session_json(session) 245 | if parsed_json: 246 | accounts_config = config_utils.read_config_file(CONFIG_PATH) 247 | session_config: dict = deepcopy(accounts_config.get(session_name, {})) 248 | session_config['user_agent'] = session_config.get('user_agent', generate_random_user_agent()) 249 | session_config['api'] = parsed_json 250 | if accounts_config.get(session_name) != session_config: 251 | await config_utils.update_session_config_in_file(session_name, session_config, CONFIG_PATH) 252 | 253 | async def run_tasks() -> None: 254 | await config_utils.restructure_config(CONFIG_PATH) 255 | await init_config_file() 256 | 257 | base_tasks = [] 258 | 259 | if settings.AUTO_UPDATE: 260 | update_manager = UpdateManager() 261 | base_tasks.append(asyncio.create_task(update_manager.run())) 262 | 263 | tg_clients = await get_tg_clients() 264 | client_tasks = [asyncio.create_task(handle_tapper_session(tg_client=tg_client)) for tg_client in tg_clients] 265 | 266 | try: 267 | if client_tasks: 268 | await asyncio.gather(*client_tasks, return_exceptions=True) 269 | 270 | for task in base_tasks: 271 | if not task.done(): 272 | task.cancel() 273 | 274 | await asyncio.gather(*base_tasks, return_exceptions=True) 275 | 276 | except asyncio.CancelledError: 277 | for task in client_tasks + base_tasks: 278 | if not task.done(): 279 | task.cancel() 280 | await asyncio.gather(*client_tasks + base_tasks, return_exceptions=True) 281 | raise 282 | 283 | async def handle_tapper_session(tg_client: UniversalTelegramClient, stats_bot: Optional[object] = None): 284 | session_name = tg_client.session_name 285 | try: 286 | logger.info(f"{session_name} | Starting session") 287 | await run_tapper(tg_client=tg_client) 288 | except InvalidSession as e: 289 | logger.error(f"Invalid session: {session_name}: {e}") 290 | await move_invalid_session_to_inactive_folder(session_name) 291 | except (AuthKeyUnregisteredError, AuthKeyDuplicatedError, AuthKeyError, 292 | SessionPasswordNeededError) as e: 293 | logger.error(f"Authentication error for Telethon session {session_name}: {e}") 294 | await move_invalid_session_to_inactive_folder(session_name) 295 | except (PyrogramAuthKeyUnregisteredError, 296 | PyrogramSessionPasswordNeededError, PyrogramSessionRevoked) as e: 297 | logger.error(f"Authentication error for Pyrogram session {session_name}: {e}") 298 | await move_invalid_session_to_inactive_folder(session_name) 299 | except Exception as e: 300 | logger.error(f"Unexpected error in session {session_name}: {e}") 301 | finally: 302 | logger.info(f"{session_name} | Session ended") -------------------------------------------------------------------------------- /bot/utils/web.py: -------------------------------------------------------------------------------- 1 | import os, subprocess, platform, signal, asyncio, stat 2 | from flask import Flask, request, jsonify, send_from_directory, render_template_string 3 | from werkzeug.utils import secure_filename 4 | 5 | PROJECT_ROOT = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 6 | UPLOAD_FOLDER = os.path.join(PROJECT_ROOT, "sessions") 7 | ALLOWED_EXTENSIONS = set(['session']) 8 | MAX_CONTENT_LENGTH = 100 * 1024 * 1024 9 | 10 | os.makedirs(UPLOAD_FOLDER, exist_ok=True) 11 | 12 | app = Flask(__name__) 13 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER 14 | app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH 15 | 16 | flask_process = None 17 | tunnel_process = None 18 | 19 | def allowed_file(filename): 20 | if '.' not in filename: 21 | return False 22 | ext = filename.rsplit('.', 1)[1].lower() 23 | return ext in ALLOWED_EXTENSIONS 24 | 25 | @app.errorhandler(413) 26 | def request_entity_too_large(error): 27 | return jsonify({'error': 'File is too large. Maximum size: 100MB'}), 413 28 | 29 | @app.after_request 30 | def add_header(response): 31 | response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0" 32 | response.headers['Pragma'] = 'no-cache' 33 | response.headers['Expires'] = '0' 34 | return response 35 | 36 | @app.route('/', methods=['GET']) 37 | def index(): 38 | return render_template_string(''' 39 | 40 | 41 | 42 | 43 | 44 | Session Manager by @mffff4 45 | 46 | 47 | 48 | 49 | 56 | 57 | 58 |
59 |

Session Manager by @mffff4

60 |
61 |
62 |
Drag & drop files here or click to upload
(Max file size: 100MB)
63 |
64 |
65 |
66 |
67 |

Files

68 |
69 | 70 | 71 | 72 | 74 | 75 | 76 | 77 | 78 |
73 | FilenameActions
79 |
80 |
81 |
82 |
83 | 84 | 199 | 200 | 201 | ''') 202 | 203 | @app.route('/upload', methods=['POST']) 204 | def upload_file(): 205 | print("Received file upload request") 206 | if 'file' not in request.files: 207 | error_msg = 'No file in the request' 208 | print(f"Error: {error_msg}") 209 | return jsonify({'error': error_msg}), 400 210 | file = request.files['file'] 211 | print(f"Uploaded file name: {file.filename}") 212 | if file.filename == '': 213 | error_msg = 'No file selected' 214 | print(f"Error: {error_msg}") 215 | return jsonify({'error': error_msg}), 400 216 | if file and allowed_file(file.filename): 217 | filename = file.filename 218 | print(f"Processed file name: {filename}") 219 | save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) 220 | print(f"File save path: {save_path}") 221 | try: 222 | file.save(save_path) 223 | success_msg = f"File '{filename}' successfully uploaded" 224 | print(success_msg) 225 | return jsonify({'success': success_msg}), 200 226 | except Exception as e: 227 | error_msg = f"Failed to save file: {str(e)}" 228 | print(f"Error: {error_msg}") 229 | return jsonify({'error': error_msg}), 500 230 | else: 231 | error_msg = 'File type not allowed' 232 | print(f"Error: {error_msg}") 233 | return jsonify({'error': error_msg}), 400 234 | 235 | def get_file_name_without_extension(filename): 236 | return os.path.splitext(filename)[0] 237 | 238 | @app.route('/files', methods=['GET']) 239 | def list_files(): 240 | try: 241 | all_files = os.listdir(UPLOAD_FOLDER) 242 | files = [f for f in all_files if os.path.isfile(os.path.join(UPLOAD_FOLDER, f)) and f.endswith('.session')] 243 | return jsonify({'files': files}), 200 244 | except Exception as e: 245 | error_msg = f'Failed to get file list: {str(e)}' 246 | print(f"Error: {error_msg}") 247 | return jsonify({'error': error_msg}), 500 248 | 249 | @app.route('/rename', methods=['POST']) 250 | def rename_file(): 251 | try: 252 | data = request.get_json() 253 | old_name = data.get('old_name', '') 254 | new_name = data.get('new_name', '') 255 | 256 | if not old_name or not new_name: 257 | return jsonify({'error': 'Invalid file names'}), 400 258 | 259 | if not old_name.endswith('.session'): 260 | old_name += '.session' 261 | 262 | if not new_name.endswith('.session'): 263 | new_name += '.session' 264 | 265 | old_path = os.path.join(UPLOAD_FOLDER, old_name) 266 | new_path = os.path.join(UPLOAD_FOLDER, new_name) 267 | 268 | if not os.path.exists(old_path): 269 | return jsonify({'error': 'Source file does not exist'}), 404 270 | 271 | if os.path.exists(new_path): 272 | return jsonify({'error': 'File with the new name already exists'}), 400 273 | 274 | os.rename(old_path, new_path) 275 | return jsonify({'success': f"File renamed to '{new_name}' successfully"}), 200 276 | except Exception as e: 277 | error_msg = f"Failed to rename file: {str(e)}" 278 | print(f"Error: {error_msg}") 279 | return jsonify({'error': error_msg}), 500 280 | 281 | @app.route('/delete/', methods=['DELETE']) 282 | def delete_file(filename): 283 | try: 284 | file_path = os.path.join(UPLOAD_FOLDER, filename) 285 | if os.path.exists(file_path): 286 | os.remove(file_path) 287 | return jsonify({'success': f"File '{filename}' successfully deleted"}), 200 288 | else: 289 | return jsonify({'error': 'File not found'}), 404 290 | except Exception as e: 291 | error_msg = f"Failed to delete file: {str(e)}" 292 | print(f"Error: {error_msg}") 293 | return jsonify({'error': error_msg}), 500 294 | 295 | @app.route('/download/', methods=['GET']) 296 | def download_file(filename): 297 | try: 298 | return send_from_directory(UPLOAD_FOLDER, filename, as_attachment=True) 299 | except Exception as e: 300 | return jsonify({'error': 'File not found'}), 404 301 | 302 | def clear_screen(): 303 | command = 'cls' if platform.system() == 'Windows' else 'clear' 304 | subprocess.call(command, shell=True) 305 | 306 | def run_serveo(): 307 | try: 308 | tunnel_process = subprocess.Popen( 309 | ["ssh", "-o", "StrictHostKeyChecking=no", "-R", "80:localhost:5000", "serveo.net"], 310 | stdout=subprocess.PIPE, 311 | stderr=subprocess.PIPE 312 | ) 313 | for line in tunnel_process.stdout: 314 | decoded_line = line.decode() 315 | if "Forwarding" in decoded_line: 316 | clear_screen() 317 | panel_url = decoded_line.split("from")[1].strip() 318 | print(f"Panel available at: {panel_url}") 319 | break 320 | except Exception as e: 321 | print(f"Error starting Serveo tunnel: {e}") 322 | 323 | async def run_web_and_tunnel(): 324 | global flask_process, tunnel_process 325 | 326 | clear_screen() 327 | 328 | os.environ["FLASK_APP"] = "bot/utils/web.py" 329 | flask_process = subprocess.Popen( 330 | ["flask", "run", "--host", "0.0.0.0", "--port", "7777"], 331 | stdout=subprocess.PIPE, 332 | stderr=subprocess.PIPE 333 | ) 334 | tunnel_process = subprocess.Popen( 335 | ["ssh", "-o", "StrictHostKeyChecking=no", "-R", "80:localhost:7777", "serveo.net"], 336 | stdout=subprocess.PIPE, 337 | stderr=subprocess.PIPE 338 | ) 339 | print("Starting web server and tunnel. Please wait...") 340 | while True: 341 | line = tunnel_process.stdout.readline().decode().strip() 342 | if "Forwarding" in line: 343 | print(f"Panel available at: {line.split()[-1]}") 344 | print("Press Ctrl+C to exit.") 345 | break 346 | while True: 347 | await asyncio.sleep(1) 348 | 349 | async def stop_web_and_tunnel(): 350 | global flask_process, tunnel_process 351 | if flask_process: 352 | flask_process.terminate() 353 | flask_process.wait() 354 | if tunnel_process: 355 | tunnel_process.terminate() 356 | tunnel_process.wait() 357 | print("Web server and tunnel stopped.") 358 | 359 | if __name__ == '__main__': 360 | print(f"UPLOAD_FOLDER path: {UPLOAD_FOLDER}") 361 | print(f"UPLOAD_FOLDER absolute path: {os.path.abspath(UPLOAD_FOLDER)}") 362 | print(f"Current working directory: {os.getcwd()}") 363 | app.run(host='0.0.0.0', port=7777) -------------------------------------------------------------------------------- /bot/utils/universal_telegram_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from better_proxy import Proxy 4 | from datetime import datetime, timedelta 5 | from random import randint, uniform 6 | from sqlite3 import OperationalError 7 | from typing import Union 8 | 9 | from opentele.tl import TelegramClient 10 | from telethon.errors import * 11 | from telethon.functions import messages, channels, account, folders 12 | from telethon.network import ConnectionTcpAbridged 13 | from telethon.types import InputBotAppShortName, InputPeerNotifySettings, InputNotifyPeer, InputUser 14 | from telethon import types as raw 15 | 16 | import pyrogram.raw.functions.account as paccount 17 | import pyrogram.raw.functions.channels as pchannels 18 | import pyrogram.raw.functions.messages as pmessages 19 | import pyrogram.raw.functions.folders as pfolders 20 | from pyrogram import Client as PyrogramClient 21 | from pyrogram.errors import * 22 | from pyrogram.raw import types as ptypes 23 | 24 | from bot.config import settings 25 | from bot.exceptions import InvalidSession 26 | from bot.utils.proxy_utils import to_pyrogram_proxy, to_telethon_proxy 27 | from bot.utils import logger, log_error, AsyncInterProcessLock, CONFIG_PATH, first_run 28 | 29 | class UniversalTelegramClient: 30 | def __init__(self, **client_params): 31 | self.session_name = None 32 | self.client: Union[TelegramClient, PyrogramClient] 33 | self.proxy = None 34 | self.is_first_run = True 35 | self.is_pyrogram: bool = False 36 | self._client_params = client_params 37 | self._init_client() 38 | self.default_val = 'bro-228618799' 39 | self.lock = AsyncInterProcessLock( 40 | os.path.join(os.path.dirname(CONFIG_PATH), 'lock_files', f"{self.session_name}.lock")) 41 | self._webview_data = None 42 | self.ref_id = settings.REF_ID if randint(1, 100) <= 70 else 'bro-228618799' 43 | 44 | def _init_client(self): 45 | try: 46 | self.client = TelegramClient(connection=ConnectionTcpAbridged, **self._client_params) 47 | self.client.parse_mode = None 48 | self.client.no_updates = True 49 | self.is_pyrogram = False 50 | self.session_name, _ = os.path.splitext(os.path.basename(self.client.session.filename)) 51 | except OperationalError: 52 | session_name = self._client_params.pop('session') 53 | self._client_params.pop('system_lang_code') 54 | self._client_params['name'] = session_name 55 | self.client = PyrogramClient(**self._client_params) 56 | 57 | self.client.no_updates = True 58 | self.client.run = lambda *args, **kwargs: None 59 | 60 | self.is_pyrogram = True 61 | self.session_name, _ = os.path.splitext(os.path.basename(self.client.name)) 62 | 63 | def set_proxy(self, proxy: Proxy): 64 | if not self.is_pyrogram: 65 | self.proxy = to_telethon_proxy(proxy) 66 | self.client.set_proxy(self.proxy) 67 | else: 68 | self.proxy = to_pyrogram_proxy(proxy) 69 | self.client.proxy = self.proxy 70 | 71 | async def get_app_webview_url(self, bot_username: str, bot_shortname: str, default_val: str) -> str: 72 | self.is_first_run = await first_run.check_is_first_run(self.session_name) 73 | return await self._pyrogram_get_app_webview_url(bot_username, bot_shortname, default_val) if self.is_pyrogram \ 74 | else await self._telethon_get_app_webview_url(bot_username, bot_shortname, default_val) 75 | 76 | async def get_webview_url(self, bot_username: str, bot_url: str, default_val: str) -> str: 77 | self.is_first_run = await first_run.check_is_first_run(self.session_name) 78 | return await self._pyrogram_get_webview_url(bot_username, bot_url, default_val) if self.is_pyrogram \ 79 | else await self._telethon_get_webview_url(bot_username, bot_url, default_val) 80 | 81 | async def join_and_mute_tg_channel(self, link: str): 82 | return await self._pyrogram_join_and_mute_tg_channel(link) if self.is_pyrogram \ 83 | else await self._telethon_join_and_mute_tg_channel(link) 84 | 85 | async def update_profile(self, first_name: str = None, last_name: str = None, about: str = None): 86 | return await self._pyrogram_update_profile(first_name=first_name, last_name=last_name, about=about) if self.is_pyrogram \ 87 | else await self._telethon_update_profile(first_name=first_name, last_name=last_name, about=about) 88 | 89 | async def _telethon_initialize_webview_data(self, bot_username: str, bot_shortname: str = None): 90 | if not self._webview_data: 91 | while True: 92 | try: 93 | peer = await self.client.get_input_entity(bot_username) 94 | bot_id = InputUser(user_id=peer.user_id, access_hash=peer.access_hash) 95 | input_bot_app = InputBotAppShortName(bot_id=bot_id, short_name=bot_shortname) 96 | self._webview_data = {'peer': peer, 'app': input_bot_app} if bot_shortname \ 97 | else {'peer': peer, 'bot': peer} 98 | return 99 | except FloodWaitError as fl: 100 | logger.warning(f"{self.session_name} | FloodWait {fl}. Waiting {fl.seconds}s") 101 | await asyncio.sleep(fl.seconds + 3) 102 | 103 | async def _telethon_get_app_webview_url(self, bot_username: str, bot_shortname: str, default_val: str) -> str: 104 | if self.proxy and not self.client._proxy: 105 | logger.critical(f"{self.session_name} | Proxy found, but not passed to TelegramClient") 106 | exit(-1) 107 | 108 | async with self.lock: 109 | try: 110 | if not self.client.is_connected(): 111 | await self.client.connect() 112 | await self._telethon_initialize_webview_data(bot_username=bot_username, bot_shortname=bot_shortname) 113 | await asyncio.sleep(uniform(1, 2)) 114 | 115 | ref_id = default_val 116 | start = {'start_param': ref_id} 117 | 118 | web_view = await self.client(messages.RequestAppWebViewRequest( 119 | **self._webview_data, 120 | platform='android', 121 | write_allowed=True, 122 | **start 123 | )) 124 | 125 | url = web_view.url 126 | 127 | if 'tgWebAppStartParam=' not in url: 128 | separator = '?' if '#' in url else '&#' 129 | insert_pos = url.find('#') if '#' in url else len(url) 130 | url = f"{url[:insert_pos]}{separator}tgWebAppStartParam={ref_id}{url[insert_pos:]}" 131 | 132 | return url 133 | 134 | except (UnauthorizedError, AuthKeyUnregisteredError): 135 | raise InvalidSession(f"{self.session_name}: User is unauthorized") 136 | except (UserDeactivatedError, UserDeactivatedBanError, PhoneNumberBannedError): 137 | raise InvalidSession(f"{self.session_name}: User is banned") 138 | 139 | except Exception: 140 | raise 141 | 142 | finally: 143 | if self.client.is_connected(): 144 | await self.client.disconnect() 145 | await asyncio.sleep(15) 146 | 147 | async def _telethon_get_webview_url(self, bot_username: str, bot_url: str, default_val: str) -> str: 148 | if self.proxy and not self.client._proxy: 149 | logger.critical(f"{self.session_name} | Proxy found, but not passed to TelegramClient") 150 | exit(-1) 151 | 152 | async with self.lock: 153 | try: 154 | if not self.client.is_connected(): 155 | await self.client.connect() 156 | await self._telethon_initialize_webview_data(bot_username=bot_username) 157 | await asyncio.sleep(uniform(1, 2)) 158 | 159 | start = {'start_param': self.get_ref_id()} if self.is_first_run else {} 160 | 161 | start_state = False 162 | async for message in self.client.iter_messages(bot_username): 163 | if r'/start' in message.text: 164 | start_state = True 165 | break 166 | await asyncio.sleep(uniform(0.5, 1)) 167 | if not start_state: 168 | await self.client(messages.StartBotRequest(**self._webview_data, **start)) 169 | await asyncio.sleep(uniform(1, 2)) 170 | 171 | web_view = await self.client(messages.RequestWebViewRequest( 172 | **self._webview_data, 173 | platform='android', 174 | from_bot_menu=False, 175 | url=bot_url, 176 | **start 177 | )) 178 | 179 | return web_view.url 180 | 181 | except (UnauthorizedError, AuthKeyUnregisteredError): 182 | raise InvalidSession(f"{self.session_name}: User is unauthorized") 183 | except (UserDeactivatedError, UserDeactivatedBanError, PhoneNumberBannedError): 184 | raise InvalidSession(f"{self.session_name}: User is banned") 185 | 186 | except Exception: 187 | raise 188 | 189 | finally: 190 | if self.client.is_connected(): 191 | await self.client.disconnect() 192 | await asyncio.sleep(15) 193 | 194 | async def _pyrogram_initialize_webview_data(self, bot_username: str, bot_shortname: str = None): 195 | if not self._webview_data: 196 | while True: 197 | try: 198 | peer = await self.client.resolve_peer(bot_username) 199 | input_bot_app = ptypes.InputBotAppShortName(bot_id=peer, short_name=bot_shortname) 200 | self._webview_data = {'peer': peer, 'app': input_bot_app} if bot_shortname \ 201 | else {'peer': peer, 'bot': peer} 202 | return 203 | except FloodWait as fl: 204 | logger.warning(f"{self.session_name} | FloodWait {fl}. Waiting {fl.value}s") 205 | await asyncio.sleep(fl.value + 3) 206 | 207 | async def _pyrogram_get_app_webview_url(self, bot_username: str, bot_shortname: str, default_val: str) -> str: 208 | if self.proxy and not self.client.proxy: 209 | logger.critical(f"{self.session_name} | Proxy found, but not passed to Client") 210 | exit(-1) 211 | 212 | async with self.lock: 213 | try: 214 | if not self.client.is_connected: 215 | await self.client.connect() 216 | await self._pyrogram_initialize_webview_data(bot_username, bot_shortname) 217 | await asyncio.sleep(uniform(1, 2)) 218 | 219 | ref_id = default_val 220 | start = {'start_param': ref_id} 221 | 222 | web_view = await self.client.invoke(pmessages.RequestAppWebView( 223 | **self._webview_data, 224 | platform='android', 225 | write_allowed=True, 226 | **start 227 | )) 228 | 229 | url = web_view.url 230 | 231 | if 'tgWebAppStartParam=' not in url: 232 | separator = '?' if '#' in url else '&#' 233 | insert_pos = url.find('#') if '#' in url else len(url) 234 | url = f"{url[:insert_pos]}{separator}tgWebAppStartParam={ref_id}{url[insert_pos:]}" 235 | 236 | return url 237 | 238 | except (Unauthorized, AuthKeyUnregistered): 239 | raise InvalidSession(f"{self.session_name}: User is unauthorized") 240 | except (UserDeactivated, UserDeactivatedBan, PhoneNumberBanned): 241 | raise InvalidSession(f"{self.session_name}: User is banned") 242 | 243 | except Exception: 244 | raise 245 | 246 | finally: 247 | if self.client.is_connected: 248 | await self.client.disconnect() 249 | await asyncio.sleep(15) 250 | 251 | async def _pyrogram_get_webview_url(self, bot_username: str, bot_url: str, default_val: str) -> str: 252 | if self.proxy and not self.client.proxy: 253 | logger.critical(f"{self.session_name} | Proxy found, but not passed to Client") 254 | exit(-1) 255 | 256 | async with self.lock: 257 | try: 258 | if not self.client.is_connected: 259 | await self.client.connect() 260 | await self._pyrogram_initialize_webview_data(bot_username) 261 | await asyncio.sleep(uniform(1, 2)) 262 | 263 | start = {'start_param': self.get_ref_id()} if self.is_first_run else {} 264 | 265 | start_state = False 266 | async for message in self.client.get_chat_history(bot_username): 267 | if r'/start' in message.text: 268 | start_state = True 269 | break 270 | await asyncio.sleep(uniform(0.5, 1)) 271 | if not start_state: 272 | await self.client.invoke(pmessages.StartBot(**self._webview_data, 273 | random_id=randint(1, 2**63), 274 | **start)) 275 | await asyncio.sleep(uniform(1, 2)) 276 | web_view = await self.client.invoke(pmessages.RequestWebView( 277 | **self._webview_data, 278 | platform='android', 279 | from_bot_menu=False, 280 | url=bot_url, 281 | **start 282 | )) 283 | 284 | return web_view.url 285 | 286 | except (Unauthorized, AuthKeyUnregistered): 287 | raise InvalidSession(f"{self.session_name}: User is unauthorized") 288 | except (UserDeactivated, UserDeactivatedBan, PhoneNumberBanned): 289 | raise InvalidSession(f"{self.session_name}: User is banned") 290 | 291 | except Exception: 292 | raise 293 | 294 | finally: 295 | if self.client.is_connected: 296 | await self.client.disconnect() 297 | await asyncio.sleep(15) 298 | 299 | async def _telethon_join_and_mute_tg_channel(self, link: str): 300 | path = link.replace("https://t.me/", "") 301 | if path == 'money': 302 | return 303 | 304 | async with self.lock: 305 | async with self.client as client: 306 | try: 307 | if path.startswith('+'): 308 | invite_hash = path[1:] 309 | result = await client(messages.ImportChatInviteRequest(hash=invite_hash)) 310 | channel_title = result.chats[0].title 311 | entity = result.chats[0] 312 | else: 313 | entity = await client.get_entity(f'@{path}') 314 | await client(channels.JoinChannelRequest(channel=entity)) 315 | channel_title = entity.title 316 | 317 | await asyncio.sleep(1) 318 | 319 | await client(account.UpdateNotifySettingsRequest( 320 | peer=InputNotifyPeer(entity), 321 | settings=InputPeerNotifySettings( 322 | show_previews=False, 323 | silent=True, 324 | mute_until=datetime.today() + timedelta(days=365) 325 | ) 326 | )) 327 | 328 | logger.info(f"{self.session_name} | Subscribed to channel: {channel_title}") 329 | except FloodWaitError as fl: 330 | logger.warning(f"{self.session_name} | FloodWait {fl}. Waiting {fl.seconds}s") 331 | return fl.seconds 332 | except Exception as e: 333 | log_error( 334 | f"{self.session_name} | (Task) Error while subscribing to tg channel {link}: {e}") 335 | 336 | await asyncio.sleep(uniform(15, 20)) 337 | return 338 | 339 | async def _pyrogram_join_and_mute_tg_channel(self, link: str): 340 | path = link.replace("https://t.me/", "") 341 | if path == 'money': 342 | return 343 | 344 | async with self.lock: 345 | async with self.client: 346 | try: 347 | if path.startswith('+'): 348 | invite_hash = path[1:] 349 | result = await self.client.invoke(pmessages.ImportChatInvite(hash=invite_hash)) 350 | channel_title = result.chats[0].title 351 | entity = result.chats[0] 352 | peer = ptypes.InputPeerChannel(channel_id=entity.id, access_hash=entity.access_hash) 353 | else: 354 | peer = await self.client.resolve_peer(f'@{path}') 355 | channel = ptypes.InputChannel(channel_id=peer.channel_id, access_hash=peer.access_hash) 356 | await self.client.invoke(pchannels.JoinChannel(channel=channel)) 357 | channel_title = path 358 | 359 | await asyncio.sleep(1) 360 | 361 | await self.client.invoke(paccount.UpdateNotifySettings( 362 | peer=ptypes.InputNotifyPeer(peer=peer), 363 | settings=ptypes.InputPeerNotifySettings( 364 | show_previews=False, 365 | silent=True, 366 | mute_until=2147483647)) 367 | ) 368 | 369 | logger.info(f"{self.session_name} | Subscribed to channel: {channel_title}") 370 | except FloodWait as e: 371 | logger.warning(f"{self.session_name} | FloodWait {e}. Waiting {e.value}s") 372 | return e.value 373 | except UserAlreadyParticipant: 374 | logger.info(f"{self.session_name} | Was already Subscribed to channel: {link}") 375 | except Exception as e: 376 | log_error( 377 | f"{self.session_name} | (Task) Error while subscribing to tg channel {link}: {e}") 378 | 379 | await asyncio.sleep(uniform(15, 20)) 380 | return 381 | 382 | async def _telethon_update_profile(self, first_name: str = None, last_name: str = None, about: str = None): 383 | update_params = { 384 | 'first_name': first_name, 385 | 'last_name': last_name, 386 | 'about': about 387 | } 388 | update_params = {k: v for k, v in update_params.items() if v is not None} 389 | if not update_params: 390 | return 391 | 392 | async with self.lock: 393 | async with self.client: 394 | try: 395 | await self.client(account.UpdateProfileRequest(**update_params)) 396 | except Exception as e: 397 | log_error( 398 | f"{self.session_name} | Failed to update profile: {e}") 399 | await asyncio.sleep(uniform(15, 20)) 400 | 401 | async def _pyrogram_update_profile(self, first_name: str = None, last_name: str = None, about: str = None): 402 | update_params = { 403 | 'first_name': first_name, 404 | 'last_name': last_name, 405 | 'about': about 406 | } 407 | update_params = {k: v for k, v in update_params.items() if v is not None} 408 | if not update_params: 409 | return 410 | 411 | async with self.lock: 412 | async with self.client: 413 | try: 414 | await self.client.invoke(paccount.UpdateProfile(**update_params)) 415 | except Exception as e: 416 | log_error( 417 | f"{self.session_name} | Failed to update profile: {e}") 418 | await asyncio.sleep(uniform(15, 20)) 419 | 420 | def get_ref_id(self) -> str: 421 | return self.ref_id 422 | 423 | async def join_telegram_channel(self, channel_data: dict) -> bool: 424 | if not settings.SUBSCRIBE_TELEGRAM: 425 | logger.warning(f"{self.session_name} | Channel subscriptions are disabled in settings") 426 | return False 427 | 428 | channel_username = channel_data.get("additional_data", {}).get("username", "") 429 | if not channel_username: 430 | logger.error(f"{self.session_name} | No channel username in task data") 431 | return False 432 | 433 | channel_username = channel_username.replace("@", "") 434 | 435 | was_connected = self.client.is_connected if not self.is_pyrogram else self.client.is_connected 436 | 437 | try: 438 | logger.info(f"{self.session_name} | Subscribing to channel {channel_username}") 439 | 440 | if not was_connected: 441 | await self.client.connect() 442 | 443 | try: 444 | if self.is_pyrogram: 445 | try: 446 | await self.client.join_chat(channel_username) 447 | chat = await self.client.get_chat(channel_username) 448 | await self._pyrogram_mute_and_archive_channel(chat.id) 449 | except UserAlreadyParticipant: 450 | logger.info(f"{self.session_name} | Already subscribed to channel {channel_username}") 451 | return True 452 | else: 453 | try: 454 | await self.client.join_chat(channel_username) 455 | chat = await self.client.get_chat(channel_username) 456 | await self._telethon_mute_and_archive_channel(chat.id) 457 | except UserAlreadyParticipant: 458 | logger.info(f"{self.session_name} | Already subscribed to channel {channel_username}") 459 | return True 460 | 461 | except FloodWait as e: 462 | wait_time = e.value if self.is_pyrogram else e.seconds 463 | logger.warning(f"{self.session_name} | FloodWait for {wait_time} seconds") 464 | await asyncio.sleep(wait_time) 465 | return await self.join_telegram_channel(channel_data) 466 | 467 | except (UserBannedInChannel, UsernameNotOccupied, UsernameInvalid) as e: 468 | logger.error(f"{self.session_name} | Error while subscribing: {str(e)}") 469 | return False 470 | 471 | except Exception as e: 472 | logger.error(f"{self.session_name} | Unknown error while subscribing: {str(e)}") 473 | return False 474 | 475 | finally: 476 | if not was_connected and (self.client.is_connected if not self.is_pyrogram else self.client.is_connected): 477 | await self.client.disconnect() 478 | 479 | return False 480 | 481 | async def _telethon_mute_and_archive_channel(self, channel_id: int) -> None: 482 | try: 483 | await self.client(account.UpdateNotifySettingsRequest( 484 | peer=InputNotifyPeer( 485 | peer=await self.client.get_input_entity(channel_id) 486 | ), 487 | settings=InputPeerNotifySettings( 488 | mute_until=2147483647 489 | ) 490 | )) 491 | logger.info(f"{self.session_name} | Notifications disabled") 492 | 493 | await self.client(folders.EditPeerFolders( 494 | folder_peers=[ 495 | raw.InputFolderPeer( 496 | peer=await self.client.get_input_entity(channel_id), 497 | folder_id=1 498 | ) 499 | ] 500 | )) 501 | logger.info(f"{self.session_name} | Channel added to archive") 502 | 503 | except Exception as e: 504 | logger.warning(f"{self.session_name} | Error while configuring channel: {str(e)}") 505 | 506 | async def _pyrogram_mute_and_archive_channel(self, channel_id: int) -> None: 507 | try: 508 | peer = await self.client.resolve_peer(channel_id) 509 | 510 | await self.client.invoke(paccount.UpdateNotifySettings( 511 | peer=ptypes.InputNotifyPeer(peer=peer), 512 | settings=ptypes.InputPeerNotifySettings( 513 | mute_until=2147483647 514 | ) 515 | )) 516 | logger.info(f"{self.session_name} | Notifications disabled") 517 | 518 | try: 519 | await self.client.invoke( 520 | pfolders.EditPeerFolders( 521 | folder_peers=[ 522 | ptypes.InputFolderPeer( 523 | peer=peer, 524 | folder_id=1 525 | ) 526 | ] 527 | ) 528 | ) 529 | logger.info(f"{self.session_name} | Channel added to archive") 530 | except Exception as e: 531 | logger.warning(f"{self.session_name} | Error while archiving: {str(e)}") 532 | 533 | except Exception as e: 534 | logger.warning(f"{self.session_name} | Error while configuring channel: {str(e)}") -------------------------------------------------------------------------------- /bot/core/tapper.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | from typing import Dict, Optional, Any, Tuple, List 4 | from urllib.parse import urlencode, unquote 5 | from aiocfscrape import CloudflareScraper 6 | from aiohttp_proxy import ProxyConnector 7 | from better_proxy import Proxy 8 | from random import uniform, randint 9 | from time import time 10 | from datetime import datetime, timezone 11 | import json 12 | import os 13 | 14 | from bot.utils.universal_telegram_client import UniversalTelegramClient 15 | from bot.utils.proxy_utils import check_proxy, get_working_proxy 16 | from bot.utils.first_run import check_is_first_run, append_recurring_session 17 | from bot.config import settings 18 | from bot.utils import logger, config_utils, CONFIG_PATH 19 | from bot.exceptions import InvalidSession 20 | 21 | class BaseBot: 22 | API_BASE_URL = "https://qlyuker.sp.yandex.ru/api" 23 | AUTH_START_URL = f"{API_BASE_URL}/auth/start" 24 | GAME_ONBOARDING_URL = f"{API_BASE_URL}/game/onboarding" 25 | GAME_SYNC_URL = f"{API_BASE_URL}/game/sync" 26 | UPGRADE_BUY_URL = f"{API_BASE_URL}/upgrades/buy" 27 | TASKS_CHECK_URL = f"{API_BASE_URL}/tasks/check" 28 | TICKETS_BUY_URL = f"{API_BASE_URL}/game/tickets/buy" 29 | 30 | DEFAULT_HEADERS = { 31 | 'Accept': '*/*', 32 | 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', 33 | 'Connection': 'keep-alive', 34 | 'Klyuk': '0110101101101100011110010111010101101011', 35 | 'Locale': 'ru', 36 | 'Origin': 'https://qlyuker.sp.yandex.ru', 37 | 'Referer': 'https://qlyuker.sp.yandex.ru/front/', 38 | 'Sec-Fetch-Dest': 'empty', 39 | 'Sec-Fetch-Mode': 'cors', 40 | 'Sec-Fetch-Site': 'same-origin', 41 | 'TGPlatform': 'ios', 42 | 'content-type': 'application/json' 43 | } 44 | 45 | UPGRADE_COOLDOWN = { 46 | "maxEnergy": 10, 47 | "coinsPerTap": 10, 48 | "promo1": 60*5, 49 | "promo2": 60*10, 50 | "promo3": 60*20, 51 | "restoreEnergy": 3600 52 | } 53 | 54 | def __init__(self, tg_client: UniversalTelegramClient): 55 | self.tg_client = tg_client 56 | if hasattr(self.tg_client, 'client'): 57 | self.tg_client.client.no_updates = True 58 | 59 | self.session_name = tg_client.session_name 60 | self._http_client: Optional[CloudflareScraper] = None 61 | self._current_proxy: Optional[str] = None 62 | self._access_token: Optional[str] = None 63 | self._is_first_run: Optional[bool] = None 64 | self._init_data: Optional[str] = None 65 | self._current_ref_id: Optional[str] = None 66 | self._game_data: Optional[Dict] = None 67 | self._cookies: Optional[str] = None 68 | self._onboarding_completed: bool = False 69 | self._team_joined: bool = False 70 | self._team_id: Optional[int] = None 71 | self._user_agent: Optional[str] = None 72 | 73 | self._accumulated_taps: int = 0 74 | self._current_energy: int = 500 75 | self._max_energy: int = 500 76 | self._last_sync_time: int = int(time()) 77 | self._current_distance: int = 0 78 | self._distance_per_tap: int = 1 79 | self._distance_per_hour: int = 0 80 | self._distance_per_sec: int = 0 81 | self._current_candies: int = 0 82 | self._current_tickets: int = 0 83 | self._next_checkpoint_position: int = 0 84 | 85 | self._energy_restores_used: int = 0 86 | self._energy_restores_max: int = 6 87 | self._last_restore_date: Optional[str] = None 88 | self._restore_energy_attempts: int = 0 89 | 90 | self._available_upgrades: Dict[str, Dict] = {} 91 | self._upgrade_last_buy_time: Dict[str, float] = {} 92 | self._pending_upgrade_check: bool = True 93 | 94 | self._available_tasks: Dict[str, Dict] = {} 95 | 96 | session_config = config_utils.get_session_config(self.session_name, CONFIG_PATH) 97 | if not all(key in session_config for key in ('api', 'user_agent')): 98 | logger.critical(f"CHECK accounts_config.json as it might be corrupted") 99 | exit(-1) 100 | 101 | self.proxy = session_config.get('proxy') 102 | if self.proxy: 103 | proxy = Proxy.from_str(self.proxy) 104 | self.tg_client.set_proxy(proxy) 105 | self._current_proxy = self.proxy 106 | 107 | self._user_agent = session_config.get('user_agent', 108 | 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 ' 109 | '(KHTML, like Gecko) Chrome/142.0.0.0 Mobile Safari/537.36') 110 | 111 | def get_ref_id(self) -> str: 112 | if self._current_ref_id is None: 113 | session_hash = sum(ord(c) for c in self.session_name) 114 | remainder = session_hash % 10 115 | if remainder < 6: 116 | self._current_ref_id = settings.REF_ID 117 | elif remainder < 9: 118 | self._current_ref_id = 'bro-228618799' 119 | else: 120 | self._current_ref_id = 'bro-228618799' 121 | return self._current_ref_id 122 | 123 | async def get_tg_web_data(self, app_name: str = "qlyukerbot", path: str = "start") -> str: 124 | try: 125 | webview_url = await self.tg_client.get_app_webview_url( 126 | app_name, 127 | path, 128 | self.get_ref_id() 129 | ) 130 | 131 | if not webview_url: 132 | raise InvalidSession("Failed to get webview URL") 133 | 134 | tg_web_data = unquote( 135 | string=webview_url.split('tgWebAppData=')[1].split('&tgWebAppVersion')[0] 136 | ) 137 | 138 | self._init_data = tg_web_data 139 | return tg_web_data 140 | 141 | except Exception as e: 142 | logger.error(f"{self.session_name} | Error getting TG Web Data: {str(e)}") 143 | raise InvalidSession("Failed to get TG Web Data") 144 | 145 | async def check_and_update_proxy(self, accounts_config: dict) -> bool: 146 | if not settings.USE_PROXY: 147 | return True 148 | 149 | if not self._current_proxy or not await check_proxy(self._current_proxy): 150 | new_proxy = await get_working_proxy(accounts_config, self._current_proxy) 151 | if not new_proxy: 152 | return False 153 | 154 | self._current_proxy = new_proxy 155 | if self._http_client and not self._http_client.closed: 156 | await self._http_client.close() 157 | 158 | proxy_conn = {'connector': ProxyConnector.from_url(new_proxy)} 159 | self._http_client = CloudflareScraper(timeout=aiohttp.ClientTimeout(60), **proxy_conn) 160 | logger.info(f"{self.session_name} | Switched to new proxy: {new_proxy}") 161 | 162 | return True 163 | 164 | async def initialize_session(self) -> bool: 165 | try: 166 | self._is_first_run = await check_is_first_run(self.session_name) 167 | if self._is_first_run: 168 | logger.info(f"{self.session_name} | First run detected for session {self.session_name}") 169 | await append_recurring_session(self.session_name) 170 | return True 171 | except Exception as e: 172 | logger.error(f"{self.session_name} | Session initialization error: {str(e)}") 173 | return False 174 | 175 | async def make_request(self, method: str, url: str, **kwargs) -> Optional[Dict]: 176 | if not self._http_client: 177 | raise InvalidSession("HTTP client not initialized") 178 | 179 | try: 180 | async with getattr(self._http_client, method.lower())(url, **kwargs) as response: 181 | if response.status == 200: 182 | return await response.json() 183 | else: 184 | if response.status != 400: 185 | response_text = "" 186 | try: 187 | response_text = await response.text() 188 | except Exception as e_text: 189 | logger.error(f"{self.session_name} | Error reading response text: {e_text}") 190 | 191 | logger.error(f"{self.session_name} | Request failed with status {response.status}. URL: {url}. Response: {response_text}") 192 | return None 193 | except Exception as e: 194 | logger.error(f"{self.session_name} | Request error: {str(e)}. URL: {url}") 195 | return None 196 | 197 | async def auth_start(self) -> bool: 198 | logger.info(f"{self.session_name} | 🔑 Attempting authorization...") 199 | try: 200 | tg_web_data = await self.get_tg_web_data() 201 | 202 | headers = self.DEFAULT_HEADERS.copy() 203 | headers['User-Agent'] = self._user_agent 204 | headers['Onboarding'] = 'null' 205 | 206 | payload = { 207 | "startData": tg_web_data 208 | } 209 | 210 | response = await self.make_request( 211 | 'post', 212 | self.AUTH_START_URL, 213 | headers=headers, 214 | json=payload 215 | ) 216 | 217 | if not response: 218 | logger.error("ኃ Auth request failed or returned no data") 219 | return False 220 | 221 | self._game_data = response 222 | 223 | if 'game' in response: 224 | game_data = response['game'] 225 | self._current_energy = game_data.get('currentEnergy', self._current_energy) 226 | self._max_energy = game_data.get('maxEnergy', self._max_energy) 227 | self._current_distance = game_data.get('currentCoins', self._current_distance) 228 | self._distance_per_tap = game_data.get('coinsPerTap', self._distance_per_tap) 229 | self._distance_per_hour = game_data.get('minePerHour', self._distance_per_hour) 230 | self._distance_per_sec = game_data.get('minePerSec', self._distance_per_sec) 231 | self._current_candies = game_data.get('currentCandies', self._current_candies) 232 | self._current_tickets = game_data.get('currentTickets', self._current_tickets) 233 | self._next_checkpoint_position = game_data.get('nextCheckpointPosition', self._next_checkpoint_position) 234 | 235 | if 'sharedConfig' in response and 'upgradeDelay' in response['sharedConfig']: 236 | self.UPGRADE_COOLDOWN = {} 237 | for level, delay in response['sharedConfig']['upgradeDelay'].items(): 238 | self.UPGRADE_COOLDOWN[int(level)] = int(delay) 239 | 240 | day_limitation_delay = response['sharedConfig'].get('dayLimitationUpgradeDelay', 3600) 241 | self.UPGRADE_COOLDOWN['restoreEnergy'] = day_limitation_delay 242 | 243 | self._update_available_upgrades() 244 | self._update_available_tasks() 245 | 246 | if hasattr(self._http_client, '_session') and hasattr(self._http_client._session, 'cookie_jar'): 247 | cookies = self._http_client._session.cookie_jar.filter_cookies(self.AUTH_START_URL) 248 | cookie_strings = [f"{key}={cookie.value}" for key, cookie in cookies.items()] 249 | self._cookies = "; ".join(cookie_strings) 250 | 251 | user_id = self._game_data.get('user', {}).get('uid') 252 | distance_to_checkpoint = self._next_checkpoint_position - self._current_distance 253 | logger.info( 254 | f"{self.session_name} | ✅ Auth successful! User ID: {user_id}, " 255 | f"📏 Distance: {self._current_distance} (to checkpoint: {distance_to_checkpoint}), " 256 | f"🍬 Candies: {self._current_candies}, 🎫 Tickets: {self._current_tickets}, " 257 | f"⚡ Energy: {self._current_energy}/{self._max_energy}" 258 | ) 259 | return True 260 | 261 | except Exception as e: 262 | logger.error(f"{self.session_name} | ኃ Error during auth start: {str(e)}") 263 | return False 264 | 265 | def _update_available_upgrades(self) -> None: 266 | if not self._game_data or 'upgrades' not in self._game_data or 'list' not in self._game_data['upgrades']: 267 | return 268 | 269 | upgrades_list = self._game_data['upgrades']['list'] 270 | self._available_upgrades = {} 271 | 272 | for upgrade in upgrades_list: 273 | upgrade_id = upgrade.get('id') 274 | if not upgrade_id: 275 | continue 276 | 277 | if upgrade_id == 'restoreEnergy': 278 | day_limitation = upgrade.get('dayLimitation', 6) 279 | current_level = upgrade.get('level', 0) 280 | self._energy_restores_max = day_limitation 281 | self._energy_restores_used = current_level 282 | 283 | self._available_upgrades[upgrade_id] = upgrade 284 | 285 | def _update_available_tasks(self) -> None: 286 | if not self._game_data or 'tasks' not in self._game_data: 287 | logger.info("🧾 No tasks data found in game_data.") 288 | self._available_tasks = {} 289 | return 290 | 291 | tasks_list = self._game_data.get('tasks', []) 292 | temp_tasks = {} 293 | for task_data in tasks_list: 294 | task_id = task_data.get('id') 295 | if task_id: 296 | temp_tasks[task_id] = task_data 297 | 298 | self._available_tasks = temp_tasks 299 | logger.info(f"{self.session_name} | 🧾 Updated available tasks: {len(self._available_tasks)} tasks loaded.") 300 | 301 | async def game_onboarding(self) -> bool: 302 | try: 303 | headers = self.DEFAULT_HEADERS.copy() 304 | headers['User-Agent'] = self._user_agent 305 | headers['Onboarding'] = '0' 306 | 307 | if self._cookies: 308 | headers['Cookie'] = self._cookies 309 | 310 | payload = {"tier": 2} 311 | 312 | response = await self.make_request( 313 | 'post', 314 | self.GAME_ONBOARDING_URL, 315 | headers=headers, 316 | json=payload 317 | ) 318 | 319 | if not response or response.get('result') != 2: 320 | logger.error(f"{self.session_name} | Failed to complete onboarding: {response}") 321 | return False 322 | 323 | logger.info(f"{self.session_name} | ✅ Onboarding completed (tier 2)") 324 | self._onboarding_completed = True 325 | return True 326 | 327 | except Exception as e: 328 | logger.error(f"{self.session_name} | Error during game onboarding: {str(e)}") 329 | return False 330 | 331 | 332 | async def sync_game(self, taps: int = 0) -> Optional[Dict]: 333 | try: 334 | headers = self.DEFAULT_HEADERS.copy() 335 | headers['User-Agent'] = self._user_agent 336 | headers['Onboarding'] = '2' 337 | 338 | if self._cookies: 339 | headers['Cookie'] = self._cookies 340 | 341 | current_time = int(time()) 342 | 343 | payload = { 344 | "currentEnergy": self._current_energy, 345 | "clientTime": current_time, 346 | "taps": taps 347 | } 348 | 349 | response = await self.make_request( 350 | 'post', 351 | self.GAME_SYNC_URL, 352 | headers=headers, 353 | json=payload 354 | ) 355 | 356 | if not response: 357 | logger.error(f"{self.session_name} | Failed to sync game data") 358 | 359 | logger.info("Trying to re-authenticate due to sync failure") 360 | if await self.auth_start(): 361 | logger.info("Re-authentication successful, retrying sync") 362 | 363 | if self._cookies: 364 | headers['Cookie'] = self._cookies 365 | 366 | response = await self.make_request( 367 | 'post', 368 | self.GAME_SYNC_URL, 369 | headers=headers, 370 | json=payload 371 | ) 372 | 373 | if not response: 374 | logger.error(f"{self.session_name} | Failed to sync game data after re-authentication") 375 | return None 376 | else: 377 | logger.error("Re-authentication failed") 378 | return None 379 | 380 | self._current_distance = response.get('currentCoins', self._current_distance) 381 | self._current_candies = response.get('currentCandies', self._current_candies) 382 | self._current_tickets = response.get('currentTickets', self._current_tickets) 383 | self._current_energy = response.get('currentEnergy', self._current_energy) 384 | self._last_sync_time = response.get('lastSync', current_time) 385 | 386 | checkpoint_reward = response.get('reward') 387 | if checkpoint_reward: 388 | reward_candies = checkpoint_reward.get('candies', 0) 389 | reward_upgrade = checkpoint_reward.get('upgrade') 390 | reward_skin = checkpoint_reward.get('skin') 391 | 392 | reward_parts = [f"{reward_candies} 🍬"] 393 | if reward_upgrade: 394 | reward_parts.append(f"upgrade: {reward_upgrade}") 395 | if reward_skin: 396 | reward_parts.append(f"skin: {reward_skin}") 397 | 398 | logger.info( 399 | f"{self.session_name} | 🎁 Checkpoint reached! " 400 | f"Reward: {', '.join(reward_parts)}" 401 | ) 402 | 403 | next_checkpoint = response.get('nextCheckpoint') 404 | if next_checkpoint: 405 | if isinstance(next_checkpoint, dict) and 'position' in next_checkpoint: 406 | self._next_checkpoint_position = next_checkpoint['position'] 407 | elif isinstance(next_checkpoint, (int, float)): 408 | self._next_checkpoint_position = int(next_checkpoint) 409 | 410 | self._pending_upgrade_check = True 411 | 412 | distance_to_checkpoint = self._next_checkpoint_position - self._current_distance if self._next_checkpoint_position > 0 else 0 413 | logger.info( 414 | f"{self.session_name} | Sync: {taps} taps, " 415 | f"📏 {self._current_distance} (to checkpoint: {distance_to_checkpoint}), " 416 | f"🍬 {self._current_candies}, ⚡ {self._current_energy}/{self._max_energy}" 417 | ) 418 | return response 419 | 420 | except Exception as e: 421 | logger.error(f"{self.session_name} | Error during game sync: {str(e)}") 422 | return None 423 | 424 | async def buy_upgrade(self, upgrade_id: str) -> Optional[Dict]: 425 | try: 426 | if not await self._is_upgrade_available(upgrade_id): 427 | logger.info(f"{self.session_name} | Upgrade {upgrade_id} is not available now") 428 | return None 429 | 430 | headers = self.DEFAULT_HEADERS.copy() 431 | headers['User-Agent'] = self._user_agent 432 | headers['Onboarding'] = '2' 433 | headers['Referer'] = 'https://qlyuker.io/upgrades' 434 | 435 | if self._cookies: 436 | headers['Cookie'] = self._cookies 437 | 438 | payload = { 439 | "upgradeId": upgrade_id 440 | } 441 | 442 | response = await self.make_request( 443 | 'post', 444 | self.UPGRADE_BUY_URL, 445 | headers=headers, 446 | json=payload 447 | ) 448 | 449 | if not response: 450 | logger.warning( 451 | f"{self.session_name} | Failed to buy upgrade {upgrade_id} " 452 | f"(may be locked or on cooldown)" 453 | ) 454 | 455 | current_level = 0 456 | if upgrade_id in self._available_upgrades: 457 | current_level = self._available_upgrades[upgrade_id].get('level', 0) 458 | 459 | cooldown = 10 460 | if current_level + 1 in self.UPGRADE_COOLDOWN: 461 | cooldown = self.UPGRADE_COOLDOWN[current_level + 1] 462 | elif isinstance(self.UPGRADE_COOLDOWN, dict) and len(self.UPGRADE_COOLDOWN) > 0: 463 | cooldown = max(self.UPGRADE_COOLDOWN.values()) 464 | 465 | self._upgrade_last_buy_time[upgrade_id] = time() 466 | 467 | if upgrade_id == 'restoreEnergy': 468 | self._restore_energy_attempts += 1 469 | if self._restore_energy_attempts >= 2: 470 | self._energy_restores_used = self._energy_restores_max 471 | 472 | return None 473 | 474 | await self._update_upgrade_after_purchase(response, upgrade_id) 475 | 476 | logger.info( 477 | f"{self.session_name} | 🛒 Successfully bought upgrade {upgrade_id}, " 478 | f"⚡ {self._current_energy}/{self._max_energy}, 🍬 {self._current_candies}" 479 | ) 480 | return response 481 | 482 | except Exception as e: 483 | logger.error(f"{self.session_name} | Error buying upgrade: {str(e)}") 484 | return None 485 | 486 | async def _update_upgrade_after_purchase(self, buy_response: Dict, upgrade_id: str) -> None: 487 | if 'currentEnergy' in buy_response: 488 | self._current_energy = buy_response.get('currentEnergy', self._current_energy) 489 | self._max_energy = buy_response.get('maxEnergy', self._max_energy) 490 | self._current_distance = buy_response.get('currentCoins', self._current_distance) 491 | self._current_candies = buy_response.get('currentCandies', self._current_candies) 492 | self._current_tickets = buy_response.get('currentTickets', self._current_tickets) 493 | self._distance_per_tap = buy_response.get('coinsPerTap', self._distance_per_tap) 494 | self._distance_per_hour = buy_response.get('minePerHour', self._distance_per_hour) 495 | self._distance_per_sec = buy_response.get('minePerSec', self._distance_per_sec) 496 | 497 | if upgrade_id == 'restoreEnergy' and 'upgrade' in buy_response: 498 | self._energy_restores_used = buy_response['upgrade'].get('level', 0) 499 | self._last_restore_date = datetime.now().strftime("%Y-%m-%d") 500 | self._restore_energy_attempts = 0 501 | 502 | if 'upgrade' in buy_response: 503 | upgrade = buy_response['upgrade'] 504 | upgrade_id = upgrade.get('id') 505 | if upgrade_id in self._available_upgrades: 506 | self._available_upgrades[upgrade_id]['level'] = upgrade.get('level', self._available_upgrades[upgrade_id].get('level', 0)) 507 | self._available_upgrades[upgrade_id]['amount'] = upgrade.get('amount', self._available_upgrades[upgrade_id].get('amount', 0)) 508 | self._available_upgrades[upgrade_id]['upgradedAt'] = upgrade.get('upgradedAt') 509 | 510 | if 'next' in buy_response: 511 | self._available_upgrades[upgrade_id]['next'] = buy_response['next'] 512 | else: 513 | self._available_upgrades[upgrade_id] = { 514 | "id": upgrade_id, 515 | "level": upgrade.get('level', 0), 516 | "amount": upgrade.get('amount', 0), 517 | "upgradedAt": upgrade.get('upgradedAt'), 518 | "next": buy_response.get('next', {}) 519 | } 520 | logger.info(f"{self.session_name} | Upgrade {upgrade_id} added after purchase") 521 | 522 | self._upgrade_last_buy_time[upgrade_id] = time() 523 | 524 | async def _is_upgrade_available(self, upgrade_id: str) -> bool: 525 | if upgrade_id != 'restoreEnergy' and upgrade_id not in self._available_upgrades: 526 | return False 527 | 528 | if upgrade_id == 'restoreEnergy': 529 | if self._restore_energy_attempts >= 2: 530 | logger.warning(f"{self.session_name} | ⚠️ Too many restore energy attempts, skipping") 531 | return False 532 | 533 | if self._energy_restores_used >= self._energy_restores_max: 534 | logger.warning( 535 | f"{self.session_name} | 🔋 Daily energy restore limit reached " 536 | f"({self._energy_restores_used}/{self._energy_restores_max})" 537 | ) 538 | return False 539 | 540 | if upgrade_id in self._upgrade_last_buy_time: 541 | current_time = time() 542 | 543 | if upgrade_id == 'restoreEnergy': 544 | cooldown = self.UPGRADE_COOLDOWN.get('restoreEnergy', 3600) 545 | else: 546 | current_level = 0 547 | if upgrade_id in self._available_upgrades: 548 | current_level = self._available_upgrades[upgrade_id].get('level', 0) 549 | 550 | cooldown = 10 551 | if current_level + 1 in self.UPGRADE_COOLDOWN: 552 | cooldown = self.UPGRADE_COOLDOWN[current_level + 1] 553 | elif isinstance(self.UPGRADE_COOLDOWN, dict) and len(self.UPGRADE_COOLDOWN) > 0: 554 | cooldown = max(self.UPGRADE_COOLDOWN.values()) 555 | 556 | last_buy_time = self._upgrade_last_buy_time[upgrade_id] 557 | elapsed_time = current_time - last_buy_time 558 | 559 | if elapsed_time < cooldown: 560 | remaining_time = cooldown - elapsed_time 561 | remaining_minutes = int(remaining_time // 60) 562 | remaining_seconds = int(remaining_time % 60) 563 | 564 | if remaining_minutes > 0: 565 | time_str = f"{remaining_minutes}m {remaining_seconds}s" 566 | else: 567 | time_str = f"{remaining_seconds}s" 568 | 569 | logger.info( 570 | f"{self.session_name} | 🕒 Upgrade {upgrade_id} on cooldown, " 571 | f"remaining: {time_str}" 572 | ) 573 | return False 574 | 575 | if upgrade_id != 'restoreEnergy': 576 | upgrade_price = self._available_upgrades[upgrade_id].get('next', {}).get('price', 0) 577 | if upgrade_price > self._current_candies: 578 | logger.info( 579 | f"{self.session_name} | 🍬 Not enough candies for upgrade {upgrade_id}, " 580 | f"need {upgrade_price}, have {self._current_candies}" 581 | ) 582 | return False 583 | 584 | return True 585 | 586 | async def _prioritize_upgrades(self) -> List[Dict]: 587 | self._update_available_upgrades() 588 | 589 | if self._current_candies < 3: 590 | return [] 591 | 592 | upgrade_scores = [] 593 | current_speed_per_sec = self._distance_per_sec 594 | 595 | excluded_upgrades = {'restoreEnergy', 'coinsPerTap'} 596 | 597 | for upgrade_id, upgrade_data in self._available_upgrades.items(): 598 | if upgrade_id in excluded_upgrades: 599 | continue 600 | 601 | if 'next' not in upgrade_data: 602 | continue 603 | 604 | if not await self._is_upgrade_available(upgrade_id): 605 | continue 606 | 607 | next_level = upgrade_data.get('next', {}) 608 | price = next_level.get('price', 0) 609 | current_level = upgrade_data.get('level', 0) 610 | increment = next_level.get('increment', 0) 611 | 612 | if price == 0 or increment == 0: 613 | continue 614 | 615 | efficiency = increment / price if price > 0 else 0 616 | 617 | payback_time_hours = (price / increment) if increment > 0 else float('inf') 618 | 619 | urgency = 1.0 620 | if current_speed_per_sec > 0: 621 | time_to_expensive = price / current_speed_per_sec 622 | if time_to_expensive < 3600: 623 | urgency = 0.7 624 | 625 | final_score = (efficiency * urgency) / (payback_time_hours + 1) 626 | 627 | upgrade_scores.append({ 628 | "upgrade_id": upgrade_id, 629 | "efficiency": efficiency, 630 | "payback_time": payback_time_hours, 631 | "final_score": final_score, 632 | "price": price, 633 | "increment": increment, 634 | "level": current_level 635 | }) 636 | 637 | sorted_upgrades = sorted( 638 | upgrade_scores, 639 | key=lambda x: (-x['final_score'], x['payback_time']) 640 | ) 641 | 642 | if sorted_upgrades: 643 | best_upgrade = sorted_upgrades[0] 644 | logger.info( 645 | f"{self.session_name} | 💡 Best upgrade: {best_upgrade['upgrade_id']}, " 646 | f"lvl: {best_upgrade['level']}, " 647 | f"price: {best_upgrade['price']} 🍬, " 648 | f"speed: +{best_upgrade['increment']}/sec" 649 | ) 650 | else: 651 | logger.info(f"{self.session_name} | 📦 No upgrades available (may need to unlock boxes first)") 652 | 653 | return sorted_upgrades 654 | 655 | async def check_and_buy_upgrades(self) -> None: 656 | logger.info(f"{self.session_name} | ▶️ Starting upgrade phase with 🍬 {self._current_candies}") 657 | prioritized_upgrades = await self._prioritize_upgrades() 658 | 659 | if not prioritized_upgrades: 660 | logger.info(f"{self.session_name} | ⏹️ No upgrades to buy or not enough candies") 661 | return 662 | 663 | upgrades_bought_count = 0 664 | for upgrade_info in prioritized_upgrades: 665 | upgrade_id = upgrade_info['upgrade_id'] 666 | price = upgrade_info['price'] 667 | 668 | if price > self._current_candies: 669 | continue 670 | 671 | logger.info(f"{self.session_name} | 🛒 Trying to buy {upgrade_id} (Price: {price} 🍬)") 672 | result = await self.buy_upgrade(upgrade_id) 673 | 674 | if result: 675 | upgrades_bought_count += 1 676 | await asyncio.sleep(2) 677 | 678 | logger.info( 679 | f"{self.session_name} | ⏹️ Upgrade phase completed. Bought: {upgrades_bought_count} upgrades. " 680 | f"Remaining 🍬 {self._current_candies}" 681 | ) 682 | 683 | async def _check_task(self, task_id: str) -> bool: 684 | task_data = self._available_tasks.get(task_id) 685 | if not task_data: 686 | logger.error(f"{self.session_name} | 🚫 Task {task_id} not found in available tasks.") 687 | return False 688 | 689 | try: 690 | headers = self.DEFAULT_HEADERS.copy() 691 | headers['User-Agent'] = self._user_agent 692 | headers['Onboarding'] = '2' 693 | if self._cookies: 694 | headers['Cookie'] = self._cookies 695 | 696 | payload = {"taskId": task_id} 697 | response = await self.make_request( 698 | 'post', 699 | self.TASKS_CHECK_URL, 700 | headers=headers, 701 | json=payload 702 | ) 703 | 704 | if not response: 705 | logger.error(f"{self.session_name} | 🚫 Failed to check task {task_id}, no response.") 706 | if task_id in self._available_tasks: 707 | self._available_tasks[task_id]['time'] = int(time()) 708 | return False 709 | 710 | if response.get('task'): 711 | self._available_tasks[task_id] = response['task'] 712 | 713 | if response.get("success") is True: 714 | reward = task_data.get('meta', {}).get('reward', 0) 715 | reward_type = task_data.get('meta', {}).get('rewardType', 'range') 716 | 717 | if 'currentCoins' in response: 718 | self._current_distance = response.get('currentCoins', self._current_distance) 719 | 720 | if 'currentCandies' in response: 721 | self._current_candies = response.get('currentCandies', self._current_candies) 722 | 723 | reward_emoji = '🍬' if reward_type == 'candy' else '📏' 724 | logger.info( 725 | f"{self.session_name} | ✅ Task {task_id} completed! " 726 | f"Reward: {reward} {reward_emoji}" 727 | ) 728 | return True 729 | else: 730 | logger.info(f"{self.session_name} | ⏳ Task {task_id} not yet completed or failed. Server time: {response.get('time')}") 731 | return False 732 | 733 | except Exception as e: 734 | logger.error(f"{self.session_name} | 🚫 Error checking task {task_id}: {str(e)}") 735 | return False 736 | 737 | async def _process_tasks(self) -> None: 738 | logger.info(f"{self.session_name} | ▶️ Starting task processing phase.") 739 | if not self._available_tasks: 740 | logger.info("🧾 No tasks available to process.") 741 | return 742 | 743 | tasks_processed_count = 0 744 | tasks_completed_count = 0 745 | supported_task_kinds = ["actionCheck", "checkPlusBenefits", "subscribeChannel"] 746 | 747 | for task_id, task_data in list(self._available_tasks.items()): 748 | task_kind = task_data.get('kind') 749 | task_title = task_data.get('title', task_id) 750 | 751 | if task_kind not in supported_task_kinds: 752 | continue 753 | 754 | last_check_time = task_data.get('time', 0) 755 | check_delay = task_data.get('meta', {}).get('checkDelay', 0) 756 | current_timestamp = int(time()) 757 | 758 | if last_check_time > 0 and current_timestamp < last_check_time + check_delay: 759 | remaining = last_check_time + check_delay - current_timestamp 760 | logger.info( 761 | f"{self.session_name} | ⏳ Task {task_id} on cooldown, " 762 | f"remaining: {remaining}s" 763 | ) 764 | continue 765 | 766 | if task_kind == "subscribeChannel": 767 | pass 768 | 769 | if task_kind in ["actionCheck", "checkPlusBenefits"]: 770 | completed = await self._check_task(task_id) 771 | if completed: 772 | tasks_completed_count += 1 773 | tasks_processed_count +=1 774 | await asyncio.sleep(randint(3, 7)) 775 | 776 | logger.info(f"{self.session_name} | ⏹️ Task processing phase completed. Checked: {tasks_processed_count}, Completed now: {tasks_completed_count}.") 777 | 778 | async def buy_tickets(self) -> None: 779 | if not self._game_data or 'user' not in self._game_data or 'yandex' not in self._game_data['user']: 780 | return 781 | 782 | logger.info(f"{self.session_name} | Yandex account detected. Starting to buy tickets with candies.") 783 | 784 | while self._current_candies >= 10: 785 | logger.info(f"{self.session_name} | Have {self._current_candies} candies. Trying to buy 1 ticket.") 786 | 787 | headers = self.DEFAULT_HEADERS.copy() 788 | headers['User-Agent'] = self._user_agent 789 | headers['Onboarding'] = '2' 790 | if self._cookies: 791 | headers['Cookie'] = self._cookies 792 | 793 | payload = {"count": 1} 794 | 795 | response = await self.make_request( 796 | 'post', 797 | self.TICKETS_BUY_URL, 798 | headers=headers, 799 | json=payload 800 | ) 801 | 802 | if response and 'result' in response: 803 | result = response['result'] 804 | old_tickets = self._current_tickets 805 | self._current_tickets = result.get('currentTickets', self._current_tickets) 806 | self._current_candies = result.get('currentCandies', self._current_candies) 807 | logger.info(f"{self.session_name} | Successfully bought {self._current_tickets - old_tickets} ticket(s). " 808 | f"Tickets: {self._current_tickets}, Candies: {self._current_candies}") 809 | await asyncio.sleep(uniform(1, 2)) 810 | else: 811 | logger.error(f"{self.session_name} | Failed to buy ticket. Stopping ticket purchase for this cycle.") 812 | break 813 | 814 | logger.info(f"{self.session_name} | Finished buying tickets. Current candies: {self._current_candies}") 815 | 816 | async def game_loop(self) -> None: 817 | if self._game_data and 'game' in self._game_data: 818 | game_data = self._game_data['game'] 819 | self._current_energy = game_data.get('currentEnergy', self._current_energy) 820 | self._max_energy = game_data.get('maxEnergy', self._max_energy) 821 | self._current_distance = game_data.get('currentCoins', self._current_distance) 822 | self._current_candies = game_data.get('currentCandies', self._current_candies) 823 | self._current_tickets = game_data.get('currentTickets', self._current_tickets) 824 | self._distance_per_tap = game_data.get('coinsPerTap', self._distance_per_tap) 825 | self._distance_per_hour = game_data.get('minePerHour', self._distance_per_hour) 826 | self._distance_per_sec = game_data.get('minePerSec', self._distance_per_sec) 827 | self._next_checkpoint_position = game_data.get('nextCheckpointPosition', self._next_checkpoint_position) 828 | 829 | sync_result = await self.sync_game(0) 830 | if not sync_result: 831 | logger.error("Initial sync failed, retrying in 30 seconds") 832 | await asyncio.sleep(30) 833 | return 834 | 835 | self._update_available_upgrades() 836 | 837 | while True: 838 | try: 839 | logger.info(f"{self.session_name} | ▶️ Starting active phase with ⚡ {self._current_energy}/{self._max_energy}") 840 | await self._active_phase() 841 | 842 | logger.info(f"{self.session_name} | ▶️ Starting task processing phase") 843 | await self._process_tasks() 844 | 845 | await self.buy_tickets() 846 | 847 | logger.info( 848 | f"{self.session_name} | ⏭️ Skipping upgrade phase (testing auto-upgrades on checkpoints). " 849 | f"Current 🍬 {self._current_candies}" 850 | ) 851 | 852 | logger.info(f"{self.session_name} | ▶️ Starting sleep phase with ⚡ {self._current_energy}/{self._max_energy}") 853 | await self._sleep_phase() 854 | 855 | logger.info("🔑 Re-authenticating after sleep phase") 856 | auth_result = await self.auth_start() 857 | if not auth_result: 858 | logger.error("🔑 Re-authentication failed, retrying in 30 seconds") 859 | await asyncio.sleep(30) 860 | continue 861 | except Exception as e: 862 | logger.error(f"{self.session_name} | 🔥 Error in game loop: {str(e)}") 863 | await asyncio.sleep(30) 864 | 865 | async def _sleep_phase(self) -> None: 866 | if self._current_energy >= self._max_energy * 0.8: 867 | logger.info(f"{self.session_name} | ✅ Energy already high, skipping sleep.") 868 | return 869 | 870 | energy_to_restore = int(self._max_energy * 0.8) - self._current_energy 871 | energy_per_sec = self._game_data.get('game', {}).get('energyPerSec', 3) 872 | if energy_per_sec <= 0: energy_per_sec = 3 873 | 874 | sleep_time = energy_to_restore / energy_per_sec 875 | max_sleep_time = 60 * 30 876 | sleep_time = min(sleep_time, max_sleep_time) 877 | sleep_time = max(sleep_time, 60) 878 | 879 | logger.info(f"{self.session_name} | 😴 Entering sleep for {int(sleep_time)}s to restore {energy_to_restore} ⚡") 880 | 881 | sleep_interval = 60 882 | total_slept = 0 883 | 884 | while total_slept < sleep_time: 885 | await asyncio.sleep(sleep_interval) 886 | total_slept += sleep_interval 887 | 888 | if total_slept % 300 == 0 or total_slept >= sleep_time: 889 | sync_result = await self.sync_game(0) 890 | if not sync_result: 891 | logger.error("🔥 Sync failed during sleep phase") 892 | continue 893 | 894 | if self._current_energy >= self._max_energy * 0.8: 895 | logger.info(f"{self.session_name} | ☀️ Energy recovered to {self._current_energy}/{self._max_energy}, ending sleep.") 896 | break 897 | 898 | logger.info( 899 | f"{self.session_name} | 💤 Sleep check: {total_slept}s passed, " 900 | f"⚡ {self._current_energy}/{self._max_energy}, " 901 | f"📏 {self._current_distance}, 🍬 {self._current_candies}" 902 | ) 903 | 904 | logger.info(f"{self.session_name} | ⏹️ Sleep phase completed. Current ⚡ {self._current_energy}/{self._max_energy}") 905 | 906 | async def run(self) -> None: 907 | if not await self.initialize_session(): 908 | raise InvalidSession("Failed to initialize session") 909 | 910 | random_delay = uniform(1, settings.SESSION_START_DELAY) 911 | logger.info(f"{self.session_name} | Bot will start in {int(random_delay)}s") 912 | await asyncio.sleep(random_delay) 913 | 914 | proxy_conn = {'connector': ProxyConnector.from_url(self._current_proxy)} if self._current_proxy else {} 915 | async with CloudflareScraper(timeout=aiohttp.ClientTimeout(60), **proxy_conn) as http_client: 916 | self._http_client = http_client 917 | 918 | while True: 919 | try: 920 | session_config = config_utils.get_session_config(self.session_name, CONFIG_PATH) 921 | if not await self.check_and_update_proxy(session_config): 922 | logger.warning('Failed to find working proxy. Sleep 5 minutes.') 923 | await asyncio.sleep(300) 924 | continue 925 | 926 | await self.process_bot_logic() 927 | 928 | except InvalidSession: 929 | raise 930 | except Exception as error: 931 | sleep_duration = uniform(60, 120) 932 | logger.error(f"{self.session_name} | Unknown error: {error}. Sleeping for {int(sleep_duration)}") 933 | await asyncio.sleep(sleep_duration) 934 | 935 | async def process_bot_logic(self) -> None: 936 | if not self._game_data: 937 | if not await self.auth_start(): 938 | logger.error("Failed to authenticate") 939 | await asyncio.sleep(60) 940 | return 941 | 942 | onboarding_level = self._game_data.get('app', {}).get('onboarding', 0) 943 | logger.info(f"{self.session_name} | Current onboarding level: {onboarding_level}") 944 | 945 | if onboarding_level == 0: 946 | if not await self.game_onboarding(): 947 | logger.error(f"{self.session_name} | Failed to complete onboarding") 948 | await asyncio.sleep(60) 949 | return 950 | 951 | await self.game_loop() 952 | 953 | async def _active_phase(self) -> None: 954 | total_taps_sent_in_phase = 0 955 | initial_energy_in_phase = self._current_energy 956 | energy_restores_used_in_phase = 0 957 | 958 | 959 | while True: 960 | if self._current_energy <= 50: 961 | energy_restored = await self.restore_energy_if_needed() 962 | if energy_restored: 963 | energy_restores_used_in_phase += 1 964 | else: 965 | break 966 | 967 | if self._current_energy > 200: 968 | taps_to_accumulate = randint(35, 45) 969 | elif self._current_energy > 100: 970 | taps_to_accumulate = randint(25, 35) 971 | else: 972 | taps_to_accumulate = randint(15, 25) 973 | 974 | taps_to_accumulate = min(taps_to_accumulate, max(0, self._current_energy - 5)) 975 | 976 | if taps_to_accumulate == 0: 977 | break 978 | 979 | sync_result = await self.sync_game(taps_to_accumulate) 980 | if not sync_result: 981 | logger.error(f"{self.session_name} | Sync failed during active phase, retrying...") 982 | await asyncio.sleep(10) 983 | continue 984 | 985 | total_taps_sent_in_phase += taps_to_accumulate 986 | await asyncio.sleep(uniform(1.5, 2.5)) 987 | 988 | distance_traveled = total_taps_sent_in_phase * self._distance_per_tap 989 | logger.info( 990 | f"{self.session_name} | ⏹️ Active phase completed. Taps: {total_taps_sent_in_phase}, " 991 | f"📏 Distance traveled: {distance_traveled}, " 992 | f"⚡ Used: {initial_energy_in_phase - self._current_energy + energy_restores_used_in_phase * self._max_energy}, " 993 | f"Restored: {energy_restores_used_in_phase}x. Current 📏 {self._current_distance}, 🍬 {self._current_candies}" 994 | ) 995 | 996 | async def restore_energy_if_needed(self) -> bool: 997 | current_date = datetime.now().strftime("%Y-%m-%d") 998 | if self._last_restore_date and self._last_restore_date != current_date: 999 | logger.info(f"{self.session_name} | ☀️ New day detected, resetting energy restores counter") 1000 | self._energy_restores_used = 0 1001 | self._restore_energy_attempts = 0 1002 | 1003 | if self._energy_restores_used >= self._energy_restores_max: 1004 | logger.info( 1005 | f"{self.session_name} | 🔋 Daily energy restore limit reached " 1006 | f"({self._energy_restores_used}/{self._energy_restores_max})" 1007 | ) 1008 | return False 1009 | 1010 | if self._restore_energy_attempts >= 2: 1011 | logger.warning(f"{self.session_name} | ⚠️ Too many restore energy attempts, skipping") 1012 | return False 1013 | 1014 | if self._pending_upgrade_check: 1015 | self._update_available_upgrades() 1016 | self._pending_upgrade_check = False 1017 | 1018 | energy_restore_data = self._available_upgrades.get('restoreEnergy') 1019 | if energy_restore_data: 1020 | day_limitation = energy_restore_data.get('dayLimitation', 6) 1021 | current_level = energy_restore_data.get('level', 0) 1022 | 1023 | self._energy_restores_max = day_limitation 1024 | self._energy_restores_used = current_level 1025 | 1026 | if current_level < day_limitation: 1027 | logger.info( 1028 | f"{self.session_name} | ⚡ Energy low ({self._current_energy}/{self._max_energy}), " 1029 | f"using restore ({current_level}/{day_limitation} used)" 1030 | ) 1031 | result = await self.buy_upgrade('restoreEnergy') 1032 | if result: 1033 | logger.info(f"{self.session_name} | ✅ Energy restored: {self._current_energy}/{self._max_energy}") 1034 | return True 1035 | 1036 | return False 1037 | 1038 | async def run_tapper(tg_client: UniversalTelegramClient): 1039 | bot = BaseBot(tg_client=tg_client) 1040 | try: 1041 | await bot.run() 1042 | except InvalidSession as e: 1043 | logger.error(f"{self.session_name} | Invalid Session: {e}") 1044 | raise --------------------------------------------------------------------------------