├── 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 | [](https://www.python.org/downloads/)
6 | [](LICENSE)
7 | [](https://telegram.org/)
8 |
9 | [🇷🇺 Русский](README_RU.md) | [🇬🇧 English](README.md)
10 |
11 | [
](https://t.me/MaineMarketBot?start=8HVF7S9K)
12 | [
](https://t.me/+vpXdTJ_S3mo0ZjIy)
13 | [
](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 | [](https://www.python.org/downloads/)
6 | [](LICENSE)
7 | [](https://telegram.org/)
8 |
9 | [🇷🇺 Русский](README_RU.md) | [🇬🇧 English](README.md)
10 |
11 | [
](https://t.me/MaineMarketBot?start=8HVF7S9K)
12 | [
](https://t.me/+vpXdTJ_S3mo0ZjIy)
13 | [
](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 |
66 |
67 |
Files
68 |
69 |
70 |
71 |
72 | |
73 | | Filename |
74 | Actions |
75 |
76 |
77 |
78 |
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
--------------------------------------------------------------------------------