├── .gitignore ├── README.md ├── config ├── data │ ├── farm.txt │ ├── proxies.txt │ └── register.txt └── settings.yaml ├── console ├── __init__.py └── main.py ├── core ├── api.py ├── auth.py ├── bot.py ├── captcha │ └── capsolver.py ├── exceptions │ └── base.py └── websocket.py ├── loader.py ├── models ├── __init__.py └── config.py ├── requirements.txt ├── run.py └── utils ├── __init__.py ├── console.py ├── file_utils.py ├── generators.py ├── imap_utils.py ├── load_config.py └── messages_generator.py /.gitignore: -------------------------------------------------------------------------------- 1 | env 2 | .env 3 | __pycache__ 4 | *.pyc 5 | *.pyo 6 | *.so 7 | *.egg-info 8 | *.dist-info 9 | *.egg 10 | *.egg-link 11 | *.so -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradient Bot 🤖 2 | 3 | 4 | Gradient Bot is an advanced automation tool designed to streamline the process of account registration and farming on Gradient Network. With its powerful features and user-friendly interface, it offers a seamless experience for users looking to maximize their efficiency on the platform. 5 | 6 | ## ✨ Features 7 | 8 | - 🔐 Automatic account registration 9 | - 🧩 Integrated captcha solving 10 | - ✉️ Email verification 11 | - 🔗 Invite code binding 12 | - 🌾 Automated farming 13 | - 📊 Export statistics to CSV 14 | - 🚀 Multi-threaded registration and export support 15 | 16 | ## 🖥️ System Requirements 17 | 18 | - Windows operating system 19 | - Stable internet connection 20 | - Valid email accounts for registration 21 | - Reliable proxies (optional but recommended) 22 | 23 | ## 🛠️ Setup Guide 24 | 25 | Prepare the necessary configuration files as outlined in the next section. 26 | 27 | ## ⚙️ Configuration 28 | 29 | ### settings.yaml 30 | 31 | This file contains the general settings for the bot. Here's an example configuration: 32 | 33 | ```yaml 34 | threads: 3 35 | invite_code: "DOIFI8" 36 | capsolver_api_key: "YOUR_CAPSOLVER_API_KEY" 37 | 38 | delay_before_start: 39 | min: 5 40 | max: 10 41 | 42 | imap_settings: 43 | rambler.ru: imap.rambler.ru 44 | hotmail.com: imap-mail.outlook.com 45 | outlook.com: imap-mail.outlook.com 46 | mail.ru: imap.mail.ru 47 | gmail.com: imap.gmail.com 48 | gmx.com: imap.gmx.com 49 | yahoo.com: imap.mail.yahoo.com 50 | gmx.net: imap.gmx.net 51 | gmx.de: imap.gmx.net 52 | ``` 53 | 54 | ### farm.txt 55 | 56 | List the accounts for farming, one per line: 57 | 58 | ``` 59 | email1@example.com:password1 60 | email2@example.com:password2 61 | ``` 62 | 63 | ### register.txt 64 | 65 | List the accounts for registration, one per line: 66 | 67 | ``` 68 | newemail1@example.com:newpassword1 69 | newemail2@example.com:newpassword2 70 | ``` 71 | 72 | ### proxies.txt 73 | 74 | List them in this format: 75 | 76 | ``` 77 | http://user:pass@ip:port 78 | http://ip:port:user:pass 79 | http://ip:port@user:pass 80 | http://user:pass:ip:port 81 | ``` 82 | 83 | ## 🚀 Usage Instructions 84 | 85 | 1. **Clone the repository**: 86 | ```sh 87 | git clone https://github.com/0ndrec/onfix_gradient.git 88 | cd onfix_gradient 89 | ``` 90 | 2. **Install the required dependencies**: 91 | ```sh 92 | pip install -r requirements.txt 93 | ``` 94 | 95 | 96 | ## ⚠️ Important Notes 97 | 98 | !! To successfully close a session you need to use the exit button or press ctrl + c, otherwise the session will remain active 99 | 100 | - 🖥️ The Gradient Bot has support for 2 active sessions. You can run 2 versions on one PC, or 1 version on 2 PCs. 101 | - 📧 Verify that your email providers are correctly configured in the `imap_settings` section of `settings.yaml`. 102 | - 🧩 Maintain sufficient balance in your CapSolver account for uninterrupted captcha solving. 103 | 104 | ## 🆘 Troubleshooting 105 | 106 | | Issue | Solution | 107 | |-------|----------| 108 | | Email verification failures | Check IMAP settings in `settings.yaml` and ensure they match your email provider's requirements. | 109 | | Captcha-related problems | Verify your CapSolver API key and account balance. | 110 | | Unexpected farming interruptions | Review console output for error messages and confirm account credentials. | 111 | 112 | --- 113 | 114 | 🌟 Thank you for choosing Gradient Bot! We're committed to enhancing your Gradient Network experience. 🌟 115 | -------------------------------------------------------------------------------- /config/data/farm.txt: -------------------------------------------------------------------------------- 1 | test@gmail.com:password -------------------------------------------------------------------------------- /config/data/proxies.txt: -------------------------------------------------------------------------------- 1 | http://proxy -------------------------------------------------------------------------------- /config/data/register.txt: -------------------------------------------------------------------------------- 1 | test@gmail.com:password -------------------------------------------------------------------------------- /config/settings.yaml: -------------------------------------------------------------------------------- 1 | threads: 5 2 | invite_code: "DOIFI8" 3 | capsolver_api_key: "CAP" 4 | 5 | delay_before_start: 6 | min: 5 7 | max: 10 8 | 9 | imap_settings: 10 | rambler.ru: imap.rambler.ru 11 | hotmail.com: imap-mail.outlook.com 12 | outlook.com: imap-mail.outlook.com 13 | mail.ru: imap.mail.ru 14 | gmail.com: imap.gmail.com 15 | gmx.com: imap.gmx.com 16 | yahoo.com: imap.mail.yahoo.com 17 | gmx.net: imap.gmx.net 18 | gmx.de: imap.gmx.net 19 | -------------------------------------------------------------------------------- /console/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .main import Console -------------------------------------------------------------------------------- /console/main.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import inquirer 5 | from inquirer.themes import GreenPassion 6 | from art import text2art 7 | from colorama import Fore 8 | from loader import config 9 | from rich.console import Console as RichConsole 10 | from rich.panel import Panel 11 | from rich.table import Table 12 | from rich import box 13 | from rich.text import Text 14 | sys.path.append(os.path.realpath('.')) 15 | 16 | class Console: 17 | MODULES = ('Register', 'Farm', 'Export statistics', 'Exit') 18 | MODULES_DATA = {'Register': 'register', 'Farm': 'farm', 'Exit': 'exit', 'Export statistics': 'export_statistics'} 19 | 20 | def __init__(self): 21 | self.rich_console = RichConsole() 22 | 23 | def show_dev_info(self): 24 | os.system('cls' if os.name == 'nt' else 'clear') 25 | title = text2art('JamBit', font='small') 26 | styled_title = Text(title, style='bold cyan') 27 | version = Text('VERSION: 1.5', style='blue') 28 | dev_panel = Panel(Text.assemble(styled_title, '\n', version), border_style='yellow', expand=False, title='[bold green]Welcome[/bold green]', subtitle='[italic]Powered by Jammer[/italic]') 29 | self.rich_console.print(dev_panel) 30 | print() 31 | 32 | @staticmethod 33 | def prompt(data: list): 34 | answers = inquirer.prompt(data, theme=GreenPassion()) 35 | return answers 36 | 37 | def get_module(self): 38 | questions = [inquirer.List('module', message=Fore.LIGHTBLACK_EX + 'Select the module', choices=self.MODULES)] 39 | answers = self.prompt(questions) 40 | return answers.get('module') 41 | 42 | def display_info(self): 43 | table = Table(title='Gradient Configuration', box=box.ROUNDED) 44 | table.add_column('Parameter', style='cyan') 45 | table.add_column('Value', style='magenta') 46 | table.add_row('Accounts to register', str(len(config.accounts_to_register))) 47 | table.add_row('Accounts to farm', str(len(config.accounts_to_farm))) 48 | table.add_row('Threads', str(config.threads)) 49 | table.add_row('Delay before start', f'{config.delay_before_start.min} - {config.delay_before_start.max} sec') 50 | panel = Panel(table, expand=False, border_style='green', title='[bold yellow]System Information[/bold yellow]', subtitle='[italic]Use arrow keys to navigate[/italic]') 51 | self.rich_console.print(panel) 52 | 53 | def build(self) -> None: 54 | self.show_dev_info() 55 | self.display_info() 56 | module = self.get_module() 57 | config.module = self.MODULES_DATA[module] -------------------------------------------------------------------------------- /core/api.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | import json 4 | import httpx 5 | from uuid import uuid4 6 | from typing import Literal, Tuple, Any 7 | from curl_cffi.requests import AsyncSession 8 | from models import Account 9 | from .exceptions.base import APIError 10 | 11 | class NodePayAPI: 12 | API_URL = 'https://api.gradient.network/api' 13 | GLOBAL_KEY = 'AIzaSyCWz-svq_InWzV9WaE3ez4XqxCE0C34ddI' 14 | 15 | def __init__(self, account: Account): 16 | self.account_data = account 17 | self.browser_id = str(uuid4()) 18 | self.session = self.setup_session() 19 | 20 | def setup_session(self) -> AsyncSession: 21 | session = AsyncSession(impersonate='chrome124', verify=False) 22 | session.timeout = 10 23 | session.headers = {'accept': 'application/json, text/plain, */*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'content-type': 'application/json', 'origin': 'https://app.gradient.network', 'priority': 'u=1, i', 'referer': 'https://app.gradient.network/', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'} 24 | if self.account_data.proxy: 25 | session.proxies = {'http': self.account_data.proxy.as_url, 'https': self.account_data.proxy.as_url} 26 | return session 27 | 28 | async def clear_request(self, url: str): 29 | session = AsyncSession(impersonate='chrome124', verify=False, timeout=10) 30 | session.proxies = self.session.proxies 31 | response = await session.get(url) 32 | return response 33 | 34 | 35 | async def send_request(self, request_type: Literal['POST', 'GET', 'OPTIONS']='POST', method: str=None, json_data: dict=None, params: dict=None, url: str=None, headers: dict=None, cookies: dict=None, verify: bool=True, max_retries: int=3, retry_delay: float=1.0): 36 | 37 | def verify_response(response_data: dict | list) -> dict | list: 38 | if isinstance(response_data, dict) and response_data.get('error'): 39 | raise APIError(f'API returned an error: {response_data}', response_data) 40 | if 'code' in str(response_data) and isinstance(response_data, dict) and (int(response_data.get('code')) != 200): 41 | raise APIError(f'API returned an error: {response_data}', response_data) 42 | return response_data 43 | for attempt in range(max_retries): 44 | try: 45 | if request_type == 'POST': 46 | if not url: 47 | response = await self.session.post(f'{self.API_URL}{method}', json=json_data, params=params, headers=headers if headers else self.session.headers, cookies=cookies) 48 | else: 49 | response = await self.session.post(url, json=json_data, params=params, headers=headers if headers else self.session.headers, cookies=cookies) 50 | elif request_type == 'OPTIONS': 51 | response = await self.session.options(url, headers=headers if headers else self.session.headers, cookies=cookies) 52 | elif not url: 53 | response = await self.session.get(f'{self.API_URL}{method}', params=params, headers=headers if headers else self.session.headers, cookies=cookies) 54 | else: 55 | response = await self.session.get(url, params=params, headers=headers if headers else self.session.headers, cookies=cookies) 56 | if verify: 57 | pass 58 | else: 59 | try: 60 | return verify_response(response.json()) 61 | except json.JSONDecodeError: 62 | return response.text 63 | return response.text 64 | except APIError: 65 | raise 66 | except Exception as error: 67 | if attempt == max_retries - 1: 68 | raise APIError(f'Failed to send request after {max_retries} attempts: {error}') 69 | await asyncio.sleep(retry_delay) 70 | raise APIError(f'Failed to send request after {max_retries} attempts') 71 | 72 | async def user_info(self) -> dict[str, Any]: 73 | response = await self.send_request(method='/user/profile', json_data={}) 74 | return response['data'] 75 | 76 | async def bind_invite_code(self, invite_code: str) -> dict[str, Any]: 77 | json_data = {'code': invite_code} 78 | response = await self.send_request(method='/user/register', json_data=json_data) 79 | return response 80 | 81 | async def get_access_token(self, refresh_token: str) -> dict[str, Any]: 82 | headers = {'accept': '*/*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'content-type': 'application/x-www-form-urlencoded', 'origin': 'https://app.gradient.network', 'priority': 'u=1, i', 'referer': 'https://app.gradient.network/', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'x-client-data': 'CKu1yQEIh7bJAQiitskBCKmdygEIq4nLAQiSocsBCJ3+zAEIhaDNAQjArM4BCNa9zgEY69PNAQ==', 'x-client-version': 'Chrome/JsCore/10.13.0/FirebaseCore-web', 'x-firebase-gmpid': '1:236765003043:web:4300552603f2d14908a096'} 83 | data = {'grant_type': 'refresh_token', 'refresh_token': refresh_token} 84 | async with httpx.AsyncClient(headers=headers) as client: 85 | response = await client.post(url='https://securetoken.googleapis.com/v1/token', params={'key': self.GLOBAL_KEY}, data=data) 86 | response = response.json() 87 | return response 88 | 89 | async def verify_email(self, code: str) -> dict[str, Any]: 90 | json_data = {'code': code} 91 | response = await self.send_request(method='/user/verify/email', json_data=json_data) 92 | return response 93 | 94 | async def send_email_verification(self, recaptcha_token: str) -> dict[str, Any]: 95 | json_data = {'code': recaptcha_token} 96 | response = await self.send_request(method='/user/send/verify/email', json_data=json_data) 97 | return response 98 | 99 | async def lookup_sign_up(self, id_token: str): 100 | headers = {'accept': '*/*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'content-type': 'application/json', 'origin': 'https://app.gradient.network', 'priority': 'u=1, i', 'user-agent': self.session.headers['user-agent'], 'x-client-data': 'CKu1yQEIh7bJAQiitskBCKmdygEIq4nLAQiSocsBCJ3+zAEIhaDNAQjArM4BCNa9zgEY69PNAQ==', 'x-client-version': 'Chrome/JsCore/10.13.0/FirebaseCore-web', 'x-firebase-gmpid': '1:236765003043:web:4300552603f2d14908a096'} 101 | json_data = {'idToken': id_token} 102 | response = await self.send_request(url='https://identitytoolkit.googleapis.com/v1/accounts:lookup', json_data=json_data, params={'key': self.GLOBAL_KEY}, headers=headers) 103 | return response 104 | 105 | async def sign_up(self): 106 | headers = {'accept': '*/*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'content-type': 'application/json', 'origin': 'https://app.gradient.network', 'priority': 'u=1, i', 'user-agent': self.session.headers['user-agent'], 'x-client-data': 'CKu1yQEIh7bJAQiitskBCKmdygEIq4nLAQiSocsBCJ3+zAEIhaDNAQjArM4BCNa9zgEY69PNAQ==', 'x-client-version': 'Chrome/JsCore/10.13.0/FirebaseCore-web', 'x-firebase-gmpid': '1:236765003043:web:4300552603f2d14908a096'} 107 | json_data = {'returnSecureToken': True, 'email': self.account_data.email, 'password': self.account_data.password, 'clientType': 'CLIENT_TYPE_WEB'} 108 | response = await self.send_request(url='https://identitytoolkit.googleapis.com/v1/accounts:signUp', json_data=json_data, params={'key': self.GLOBAL_KEY}, headers=headers) 109 | return response 110 | 111 | async def nodes_list(self) -> list[dict[str, Any]]: 112 | json_data = {'page': 1, 'size': 12, 'field': 'active', 'direction': 0, 'active': '', 'banned': ''} 113 | response = await self.send_request(method='/sentrynode/list', json_data=json_data) 114 | return response['data'] 115 | 116 | async def send_node_status(self): 117 | headers = {'accept': 'application/json, text/plain, */*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'authorization': self.session.headers['authorization'], 'priority': 'u=1, i', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'user-agent': self.session.headers['user-agent']} 118 | return await self.send_request(request_type='GET', method='/status', headers=headers) 119 | 120 | async def get_node_info(self, node_id: str) -> dict[str, Any]: 121 | response = await self.send_request(request_type='GET', method=f'/sentrynode/get/{node_id}') 122 | return response['data'] 123 | 124 | async def register_node(self) -> dict[str, Any]: 125 | headers = {'accept': 'application/json, text/plain, */*', 'accept-language': 'ar-DZ,ar;q=0.9,en-US;q=0.8,en;q=0.7', 'authorization': self.session.headers['authorization'], 'content-type': 'application/x-www-form-urlencoded', 'origin': 'chrome-extension://caacbgbklghmpodbdafajbgdnegacfmo', 'priority': 'u=1, i', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'user-agent': self.session.headers['user-agent']} 126 | return await self.send_request(request_type='POST', method='/sentrynode/register', headers=headers) 127 | 128 | async def sign_in(self) -> dict[str, Any]: 129 | json_data = {'returnSecureToken': True, 'email': self.account_data.email, 'password': self.account_data.password, 'clientType': 'CLIENT_TYPE_WEB'} 130 | response = await self.send_request(url='https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword', json_data=json_data, params={'key': self.GLOBAL_KEY}) 131 | if response.get('idToken'): 132 | self.session.headers['authorization'] = f"Bearer {response['idToken']}" 133 | return response 134 | raise APIError(f'Failed to sign in: {response}') -------------------------------------------------------------------------------- /core/auth.py: -------------------------------------------------------------------------------- 1 | 2 | import aiohttp 3 | import json 4 | import os 5 | from loguru import logger 6 | import inquirer 7 | 8 | class ClientAuth: 9 | 10 | def __init__(self): 11 | self.username = None 12 | self.password = None 13 | self.credentials_file = 'credentials.json' 14 | self.api_url = 'https://gradient-auth-api.onrender.com' 15 | 16 | async def authenticate_user(self, username: str, password: str) -> tuple[bool, str]: 17 | async with aiohttp.ClientSession() as session: 18 | async with session.post(f'{self.api_url}/login', json={'username': username, 'password': password}, ssl=False) as response: 19 | if response.status == 200: 20 | self.username = username 21 | self.password = password 22 | return (True, 'Login successful') 23 | else: 24 | error_details = await response.json() 25 | return (False, error_details.get('detail', 'Unknown error occurred')) 26 | 27 | async def deactivate_session(self): 28 | if self.username and self.password: 29 | async with aiohttp.ClientSession() as session: 30 | async with session.post(f'{self.api_url}/logout', json={'username': self.username, 'password': self.password}, ssl=False) as response: 31 | if response.status == 200: 32 | logger.info('>> Successfully logged out') 33 | else: 34 | error_details = await response.json() 35 | logger.error(f"Failed to logout: {error_details.get('detail', 'Unknown error')}") 36 | 37 | def save_credentials(self): 38 | credentials = {'username': self.username, 'password': self.password} 39 | try: 40 | with open(self.credentials_file, 'w') as f: 41 | json.dump(credentials, f) 42 | except IOError: 43 | logger.warning('Could not save credentials') 44 | 45 | def load_credentials(self): 46 | if os.path.exists(self.credentials_file): 47 | try: 48 | with open(self.credentials_file, 'r') as f: 49 | credentials = json.load(f) 50 | return (credentials.get('username'), credentials.get('password')) 51 | except IOError: 52 | logger.warning('Could not load credentials') 53 | return (None, None) 54 | 55 | async def login(self): 56 | saved_username, saved_password = self.load_credentials() 57 | if saved_username and saved_password: 58 | logger.info(f'Found saved credentials for user: {saved_username}') 59 | use_saved = inquirer.confirm('Do you want to use saved credentials?', default=True) 60 | if use_saved: 61 | success, message = await self.authenticate_user(saved_username, saved_password) 62 | if success: 63 | self.username = saved_username 64 | self.password = saved_password 65 | logger.info(f'User {saved_username} successfully authenticated') 66 | return True 67 | logger.error(f'Authentication failed: {message}') 68 | if message != 'Maximum number of sessions reached': 69 | os.remove(self.credentials_file) 70 | else: 71 | return False 72 | return await self.manual_login() 73 | 74 | async def manual_login(self) -> bool: 75 | questions = [inquirer.Text('username', message='Enter username'), inquirer.Password('password', message='Enter password')] 76 | answers = inquirer.prompt(questions) 77 | success, message = await self.authenticate_user(answers['username'], answers['password']) 78 | if success: 79 | self.save_credentials() 80 | logger.info(f"User {answers['username']} successfully authenticated") 81 | return True 82 | logger.error(f'Authentication failed: {message}') 83 | return False 84 | 85 | async def run(self) -> bool: 86 | if not await self.login(): 87 | return False 88 | return True -------------------------------------------------------------------------------- /core/bot.py: -------------------------------------------------------------------------------- 1 | 2 | from loader import captcha_solver, config 3 | from models import Account 4 | from utils import * 5 | from .websocket import WebSocketClient 6 | from .api import NodePayAPI 7 | from .exceptions.base import APIError 8 | 9 | class Bot(NodePayAPI): 10 | def __init__(self, account: Account): 11 | super().__init__(account) 12 | 13 | async def get_recaptcha_token(self) -> str: 14 | for _ in range(3): 15 | logger.info(f'Account: {self.account_data.email} | Solving captcha...') 16 | result, status = await captcha_solver.solve_recaptcha() 17 | if status: 18 | logger.success(f'Account: {self.account_data.email} | Captcha solved') 19 | return result 20 | logger.error(f'Account: {self.account_data.email} | {result} | Retrying...') 21 | await asyncio.sleep(3) 22 | else: # inserted 23 | raise APIError('Captcha solving failed after 3 attempts') 24 | 25 | async def process_registration(self) -> bool: 26 | try: 27 | if not await check_if_email_valid(self.account_data.imap_server, self.account_data.email, self.account_data.password): 28 | return False 29 | logger.info(f'Account: {self.account_data.email} | Registering...') 30 | tokens = await self.sign_up() 31 | await self.lookup_sign_up(tokens['idToken']) 32 | recaptcha_token = await self.get_recaptcha_token() 33 | self.session.headers['authorization'] = f"Bearer {tokens['idToken']}" 34 | response = await self.send_email_verification(recaptcha_token) 35 | if not response or response.get('code', 0)!= 200: 36 | logger.error(f'Account: {self.account_data.email} | Failed to send email verification: {response}') 37 | return False 38 | else: 39 | logger.info(f'Account: {self.account_data.email} | Email verification sent') 40 | code = await check_email_for_code(self.account_data.imap_server, self.account_data.email, self.account_data.password) 41 | if code is None: 42 | logger.error(f'Account: {self.account_data.email} | Failed to get email verification code') 43 | return False 44 | except APIError as error: 45 | if error.error_message and error.error_message.strip() == 'EMAIL_EXISTS': 46 | logger.warning(f'Account: {self.account_data.email} | Email already registered') 47 | return True 48 | logger.error(f'Account: {self.account_data.email} | Failed to register (APIError): {(error.error_message if error.error_message else str(error))}') 49 | except Exception as error: 50 | logger.error(f'Account: {self.account_data.email} | Failed to register: {error}') 51 | return False 52 | else: # inserted 53 | await self.verify_email(code) 54 | logger.success(f'Account: {self.account_data.email} | Email verified') 55 | tokens = await self.get_access_token(tokens['refreshToken']) 56 | self.session.headers['authorization'] = f"Bearer {tokens['access_token']}" 57 | await self.bind_invite_code(config.invite_code) 58 | logger.success(f'Account: {self.account_data.email} | Registered successfully') 59 | return True 60 | 61 | async def process_farming(self): 62 | try: 63 | await self.perform_farming_actions() 64 | except Exception as error: 65 | logger.error(f'Account: {self.account_data.email} | Unknown exception while farming: {error} | Stopped') 66 | 67 | async def close_session(self): 68 | try: 69 | await self.session.close() 70 | except Exception as error: 71 | logger.debug(f'Account: {self.account_data.email} | Failed to close session: {error}') 72 | 73 | async def run_websocket_client(self, node_credentials: dict): 74 | client = WebSocketClient(self.account_data) 75 | return await client.connect(client_id=node_credentials['clientid'], username=node_credentials['username'], password=node_credentials['password']) 76 | 77 | async def verify_node(self, client_id: str): 78 | pass 79 | try: 80 | pass # postinserted 81 | logger.error(f'Account: {self.account_data.email} | Node banned, stopping...') 82 | return 83 | except Exception as error: 84 | logger.error(f'Account: {self.account_data.email} | Failed to verify node: {error}') 85 | await asyncio.sleep(1800) 86 | 87 | async def process_login(self) -> bool: 88 | try: 89 | self.session = self.setup_session() 90 | logger.info(f'Account: {self.account_data.email} | Logging in...') 91 | await self.sign_in() 92 | logger.info(f'Account: {self.account_data.email} | Successfully logged in') 93 | return True 94 | except APIError as error: 95 | logger.error(f'Account: {self.account_data.email} | {error}') 96 | return False 97 | 98 | async def process_get_user_info(self) -> dict: 99 | try: 100 | if not await self.process_login(): 101 | return {} 102 | user_info = await self.user_info() 103 | logger.success(f'Account: {self.account_data.email} | User info fetched') 104 | return user_info 105 | except APIError as error: 106 | logger.error(f'Account: {self.account_data.email} | {error}') 107 | return {} 108 | 109 | async def perform_farming_actions(self): 110 | if not await self.process_login(): 111 | return 112 | node_credentials = await self.register_node() 113 | logger.info(f"Account: {self.account_data.email} | Node registered with ID: {node_credentials['clientid']}") 114 | websocket_task = asyncio.create_task(self.run_websocket_client(node_credentials)) 115 | await asyncio.sleep(30) 116 | verify_task = asyncio.create_task(self.verify_node(node_credentials['clientid'])) 117 | while True: 118 | if websocket_task.done() or websocket_task.cancelled(): 119 | if not verify_task.cancelled(): 120 | verify_task.cancel() 121 | if not websocket_task.cancelled(): 122 | websocket_task.cancel() 123 | logger.info(f'Account: {self.account_data.email} | Retry farming in 10m') 124 | await asyncio.sleep(600) 125 | self.session = self.setup_session() 126 | return await self.perform_farming_actions() 127 | else: # inserted 128 | if verify_task.done() or verify_task.cancelled(): 129 | logger.info(f'Account: {self.account_data.email} | Stopped farming') 130 | if not websocket_task.cancelled(): 131 | websocket_task.cancel() 132 | if not verify_task.cancelled(): 133 | verify_task.cancel() 134 | return None 135 | await asyncio.sleep(3) -------------------------------------------------------------------------------- /core/captcha/capsolver.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | import httpx 4 | from typing import Any, Tuple 5 | 6 | class CapsolverSolver: 7 | 8 | def __init__(self, api_key: str): 9 | self.api_key = api_key 10 | self.client = httpx.AsyncClient(timeout=10) 11 | self.create_task_url = 'https://api.capsolver.com/createTask' 12 | self.get_task_result_url = 'https://api.capsolver.com/getTaskResult' 13 | 14 | async def solve_recaptcha(self) -> Tuple[str, bool]: 15 | try: 16 | captcha_data = {'clientKey': self.api_key, 'task': {'type': 'ReCaptchaV2EnterpriseTaskProxyLess', 'websiteURL': 'https://app.gradient.network/', 'websiteKey': '6Lfe5TAqAAAAAI3mJZFYU17Rzjh9DB5KDRReuqYV'}} 17 | resp = await self.client.post(self.create_task_url, json=captcha_data) 18 | if resp.status_code == 200: 19 | return await self.get_captcha_result(resp.json().get('taskId')) 20 | except httpx.RequestError as err: 21 | return (str(err), False) 22 | except Exception as err: 23 | return (str(err), False) 24 | else: 25 | return ('Failed to create captcha task', False) 26 | 27 | async def get_captcha_result(self, task_id: str) -> Tuple[Any, bool]: 28 | try: 29 | for _ in range(20): 30 | resp = await self.client.post(self.get_task_result_url, json={'clientKey': self.api_key, 'taskId': str(task_id)}) 31 | if resp.status_code == 200: 32 | result = resp.json() 33 | if result.get('status') == 'failed': 34 | return (result.get('errorDescription'), False) 35 | if result.get('status') == 'ready': 36 | return (result['solution'].get('gRecaptchaResponse', ''), True) 37 | except httpx.RequestError as err: 38 | return (str(err), False) 39 | except Exception as err: 40 | return (str(err), False) 41 | else: 42 | await asyncio.sleep(5) 43 | return ('Max time for solving exhausted', False) -------------------------------------------------------------------------------- /core/exceptions/base.py: -------------------------------------------------------------------------------- 1 | 2 | class APIError(Exception): 3 | BASE_MESSAGES = ['refresh your captcha!!', 'Incorrect answer. Try again!'] 4 | pass 5 | 6 | def __init__(self, error: str, response_data: dict=None): 7 | self.error = error 8 | self.response_data = response_data 9 | 10 | @property 11 | def error_message(self) -> str: 12 | if self.response_data: 13 | try: 14 | return self.response_data['error']['message'] 15 | except KeyError: 16 | return str(self.error) 17 | return None 18 | 19 | def __str__(self): 20 | return self.error -------------------------------------------------------------------------------- /core/websocket.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | import ssl 4 | from typing import Dict 5 | import aiohttp 6 | from aiohttp import ClientSession, ClientWebSocketResponse, WSMessage, WSMsgType 7 | from loguru import logger 8 | from models import Account 9 | from utils.messages_generator import MQTTMessageGenerator 10 | 11 | class WebSocketClient: 12 | WSS_URL = 'wss://wss.gradient.network/mqtt' 13 | 14 | def __init__(self, account: Account): 15 | self.ssl_context = ssl.create_default_context() 16 | self.ssl_context.check_hostname = False 17 | self.ssl_context.verify_mode = ssl.CERT_NONE 18 | self.account_data = account 19 | self.retry_attempts = 0 20 | self.headers = {'Pragma': 'no-cache', 'Origin': 'chrome-extension://caacbgbklghmpodbdafajbgdnegacfmo', 'Accept-Language': 'en-US,en;q=0.9', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Cache-Control': 'no-cache', 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits'} 21 | 22 | def add_retry_attempt(self) -> bool: 23 | self.retry_attempts += 1 24 | if self.retry_attempts >= 10: 25 | logger.error(f'Account: {self.account_data.email} | Too many retry attempts, stopping...') 26 | return False 27 | return True 28 | 29 | async def connect(self, client_id: str, username: str, password: str) -> None: 30 | while True: 31 | try: 32 | async with ClientSession() as session: 33 | async with session.ws_connect(self.WSS_URL, ssl=self.ssl_context, headers=self.headers, protocols=['mqtt'], proxy=self.account_data.proxy.as_url, timeout=30) as websocket: 34 | await self.handle_connection(websocket, client_id, username, password) 35 | break 36 | except aiohttp.ClientError as error: 37 | logger.error(f'Account: {self.account_data.email} | Connection error: {error}') 38 | if not self.add_retry_attempt(): 39 | break 40 | except asyncio.CancelledError: 41 | logger.info(f'Account: {self.account_data.email} | Connection cancelled forced, websocket closed') 42 | return 43 | except Exception as error: 44 | logger.error(f'Account: {self.account_data.email} | Unexpected websocket error: {error}') 45 | if not self.add_retry_attempt(): 46 | break 47 | logger.info(f'Account: {self.account_data.email} | Reconnecting in 5 seconds...') 48 | await asyncio.sleep(5) 49 | 50 | async def handle_connection(self, websocket: ClientWebSocketResponse, client_id: str, username: str, password: str) -> None: 51 | generator = MQTTMessageGenerator(client_id=client_id, username=username, password=password) 52 | logger.info(f'Account: {self.account_data.email} | WebSocket connection established | Client ID: {client_id} | Sending initial messages...') 53 | try: 54 | await self.send_initial_messages(websocket, generator) 55 | logger.success(f'Account: {self.account_data.email} | Initial messages sent successfully | Starting messages loop...') 56 | await self.messages_loop(websocket, generator) 57 | except Exception as e: 58 | logger.error(f'Account: {self.account_data.email} | Error while handling connection: {e}') 59 | 60 | async def send_initial_messages(self, websocket: ClientWebSocketResponse, generator: MQTTMessageGenerator) -> None: 61 | messages = [('login', generator.generate_login_message()), ('task', generator.generate_task_message()), ('ping', generator.generate_ping_message())] 62 | for msg_type, msg in messages: 63 | logger.debug(f'Account: {self.account_data.email} | Sending {msg_type} message') 64 | await websocket.send_bytes(msg) 65 | await self.handle_and_receive_message(websocket) 66 | await asyncio.sleep(2) 67 | self.retry_attempts = 0 68 | 69 | async def messages_loop(self, websocket: ClientWebSocketResponse, generator: MQTTMessageGenerator) -> None: 70 | if False: 71 | await websocket.send_bytes(generator.generate_ping_message()) 72 | await self.handle_and_receive_message(websocket) 73 | await asyncio.sleep(1) 74 | 75 | async def handle_and_receive_message(self, websocket: ClientWebSocketResponse) -> None: 76 | try: 77 | msg = await websocket.receive() 78 | if isinstance(msg, WSMessage): 79 | if msg.type == WSMsgType.BINARY: 80 | return 81 | if msg.type == WSMsgType.TEXT: 82 | logger.info(f'Account: {self.account_data.email} | Received text message: {msg.data}') 83 | elif msg.type == WSMsgType.CLOSED: 84 | logger.warning(f'Account: {self.account_data.email} | WebSocket connection closed') 85 | raise asyncio.CancelledError('WebSocket closed') 86 | elif msg.type == WSMsgType.ERROR: 87 | logger.error(f'Account: {self.account_data.email} | WebSocket error: {msg.data}') 88 | raise Exception(f'WebSocket error: {msg.data}') 89 | except Exception as error: 90 | logger.error(f'Account: {self.account_data.email} | Error in handle_message: {error}') 91 | -------------------------------------------------------------------------------- /loader.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from utils import load_config 3 | from core.captcha import capsolver 4 | config = load_config() 5 | captcha_solver = capsolver.CapsolverSolver(api_key=config.capsolver_api_key) 6 | semaphore = asyncio.Semaphore(config.threads) -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .config import * -------------------------------------------------------------------------------- /models/config.py: -------------------------------------------------------------------------------- 1 | 2 | from better_proxy import Proxy 3 | from pydantic import BaseModel, PositiveInt, ConfigDict 4 | 5 | class Account(BaseModel): 6 | model_config = ConfigDict(arbitrary_types_allowed=True) 7 | email: str 8 | password: str 9 | imap_server: str = '' 10 | proxy: Proxy 11 | 12 | class Config(BaseModel): 13 | model_config = ConfigDict(arbitrary_types_allowed=True) 14 | 15 | class DelayBeforeStart(BaseModel): 16 | min: int 17 | max: int 18 | accounts_to_register: list[Account] = [] 19 | accounts_to_farm: list[Account] = [] 20 | capsolver_api_key: str 21 | invite_code: str 22 | delay_before_start: DelayBeforeStart 23 | threads: PositiveInt 24 | imap_settings: dict[str, str] 25 | module: str = '' -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | art 3 | better-proxy 4 | bs4 5 | colorama 6 | curl_cffi 7 | httpx 8 | imap_tools 9 | inquirer 10 | loguru 11 | pydantic 12 | rich 13 | urllib3 14 | pyyaml 15 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | 2 | global active_accounts # inserted 3 | import asyncio 4 | import random 5 | import signal 6 | import sys 7 | from asyncio import Lock 8 | from loguru import logger 9 | from loader import config, semaphore 10 | from core.bot import Bot 11 | from core.auth import ClientAuth 12 | from models import Account 13 | from utils import export_results, setup, export_statistics 14 | from console import Console 15 | active_accounts = 0 16 | active_accounts_lock = Lock() 17 | 18 | async def process_registration(account: Account) -> tuple[str, str, bool]: 19 | async with semaphore: 20 | if config.delay_before_start.min > 0 and config.delay_before_start.max > 0: 21 | delay = random.randint(config.delay_before_start.min, config.delay_before_start.max) 22 | logger.info(f'Account: {account.email} | Sleeping for {delay} seconds before starting...') 23 | await asyncio.sleep(delay) 24 | bot = Bot(account) 25 | result = await bot.process_registration() 26 | await bot.close_session() 27 | return (account.email, account.password, result) 28 | 29 | async def process_farming(account: Account) -> None: 30 | global active_accounts # inserted 31 | if config.delay_before_start.min > 0 and config.delay_before_start.max > 0: 32 | delay = random.randint(config.delay_before_start.min, config.delay_before_start.max) 33 | logger.info(f'Account: {account.email} | Sleeping for {delay} seconds before starting...') 34 | await asyncio.sleep(delay) 35 | async with active_accounts_lock: 36 | active_accounts += 1 37 | logger.info(f'Active accounts: {active_accounts}') 38 | try: 39 | bot = Bot(account) 40 | await bot.process_farming() 41 | finally: # inserted 42 | async with active_accounts_lock: 43 | active_accounts -= 1 44 | logger.info(f'Active accounts: {active_accounts}') 45 | 46 | async def process_export_statistics(account: Account) -> dict: 47 | async with semaphore: 48 | if config.delay_before_start.min > 0 and config.delay_before_start.max > 0: 49 | delay = random.randint(config.delay_before_start.min, config.delay_before_start.max) 50 | logger.info(f'Account: {account.email} | Sleeping for {delay} seconds before starting...') 51 | await asyncio.sleep(delay) 52 | bot = Bot(account) 53 | user_info = await bot.process_get_user_info() 54 | await bot.close_session() 55 | return user_info 56 | 57 | async def cleanup(auth_client: ClientAuth): 58 | await auth_client.deactivate_session() 59 | 60 | async def run(auth_client): 61 | try: 62 | while True: 63 | Console().build() 64 | if config.module == 'register': 65 | if not config.accounts_to_register: 66 | logger.error('No accounts to register') 67 | else: # inserted 68 | tasks = [asyncio.create_task(process_registration(account)) for account in config.accounts_to_register] 69 | results = await asyncio.gather(*tasks) 70 | export_results(results, 'register') 71 | else: # inserted 72 | if config.module == 'farm': 73 | if not config.accounts_to_farm: 74 | logger.error('No accounts to farm') 75 | else: # inserted 76 | random.shuffle(config.accounts_to_farm) 77 | tasks = [asyncio.create_task(process_farming(account)) for account in config.accounts_to_farm] 78 | await asyncio.gather(*tasks) 79 | else: # inserted 80 | if config.module == 'export_statistics': 81 | if not config.accounts_to_farm: 82 | logger.error('No accounts to export statistics') 83 | else: # inserted 84 | tasks = [asyncio.create_task(process_export_statistics(account)) for account in config.accounts_to_farm] 85 | users_data = await asyncio.gather(*tasks) 86 | export_statistics(users_data) 87 | else: # inserted 88 | if config.module == 'exit': 89 | pass 90 | input('\n\nPress Enter to continue...') 91 | except Exception as err: 92 | logger.debug(f'An error occurred: {err}') 93 | finally: # inserted 94 | pass # postinserted 95 | await cleanup(auth_client) 96 | 97 | async def main(): 98 | auth_client = ClientAuth() 99 | status = await auth_client.run() 100 | if not status: 101 | await auth_client.deactivate_session() 102 | return 103 | else: # inserted 104 | try: 105 | await run(auth_client) 106 | except asyncio.CancelledError: 107 | logger.info('Main task was cancelled') 108 | except Exception as e: 109 | logger.error(f'An error occurred: {e}') 110 | finally: 111 | await cleanup(auth_client) 112 | 113 | def handle_interrupt(signum, frame): 114 | logger.info('Received interrupt signal. Cancelling tasks...') 115 | for task in asyncio.all_tasks(loop=asyncio.get_event_loop()): 116 | try: 117 | task.cancel() 118 | except asyncio.CancelledError: 119 | continue 120 | if __name__ == '__main__': 121 | if sys.platform == 'win32': 122 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 123 | setup() 124 | signal.signal(signal.SIGINT, handle_interrupt) 125 | signal.signal(signal.SIGTERM, handle_interrupt) 126 | try: 127 | asyncio.run(main()) 128 | except Exception as error: 129 | logger.debug(f'An error occurred: {error}') 130 | input('\n\nPress Enter to exit...') -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .load_config import load_config 3 | from .console import * 4 | from .file_utils import * 5 | from .imap_utils import * 6 | from .generators import * -------------------------------------------------------------------------------- /utils/console.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import urllib3 5 | from art import tprint 6 | from loguru import logger 7 | 8 | def setup(): 9 | urllib3.disable_warnings() 10 | logger.remove() 11 | logger.add(sys.stdout, colorize=True, format='{time:HH:mm:ss} | {level: <8} | - {message}') 12 | logger.add('./logs/logs.log', rotation='1 day', retention='7 days') 13 | 14 | def show_dev_info(): 15 | os.system('cls') 16 | tprint('JamBit') 17 | print('\x1b[36mChannel: \x1b[34mhttps://t.me/JamBitPY\x1b[34m') 18 | print('\x1b[36mGitHub: \x1b[34mhttps://github.com/Jaammerr\x1b[34m') 19 | print() -------------------------------------------------------------------------------- /utils/file_utils.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import csv 4 | from loguru import logger 5 | 6 | def export_results(results: list, module: str) -> None: 7 | if not os.path.exists('./results'): 8 | os.makedirs('./results') 9 | if module == 'register': 10 | success_txt = open('./results/registration_success.txt', 'w') 11 | failed_txt = open('./results/registration_failed.txt', 'w') 12 | for email, account_password, status in results: 13 | if status: 14 | success_txt.write(f'{email}:{account_password}\n') 15 | else: 16 | failed_txt.write(f'{email}:{account_password}\n') 17 | logger.debug('Results exported to results folder') 18 | 19 | def export_statistics(users_data: list[dict]): 20 | if not os.path.exists('./results'): 21 | os.makedirs('./results') 22 | unique_users = [] 23 | for data in users_data: 24 | if data and data['id'] not in unique_users: 25 | unique_users.append(data) 26 | with open('./results/statistics.csv', 'w', newline='') as file: 27 | fieldnames = ['ID', 'Username', 'Email', 'Invite code', 'Total points', 'Referrals'] 28 | writer = csv.DictWriter(file, fieldnames=fieldnames) 29 | writer.writeheader() 30 | logger.debug('Exporting statistics to CSV file...') 31 | for data in unique_users: 32 | if data: 33 | writer.writerow({'ID': data['id'], 'Username': data['name'], 'Email': data['email'], 'Invite code': data['code'], 'Total points': int(data['point']['total']) / 100000, 'Referrals': data['stats']['invitee']}) 34 | logger.debug('Export completed successfully.') -------------------------------------------------------------------------------- /utils/generators.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | import string 4 | 5 | def generate_password(length: int=12) -> str: 6 | if length < 8: 7 | raise ValueError('Password length should be at least 8 characters') 8 | lowercase = string.ascii_lowercase 9 | uppercase = string.ascii_uppercase 10 | digits = string.digits 11 | symbols = '!@#$%^&*' 12 | password = [random.choice(lowercase), random.choice(uppercase), random.choice(digits), random.choice(symbols)] 13 | for _ in range(length - 4): 14 | password.append(random.choice(lowercase + uppercase + digits + symbols)) 15 | random.shuffle(password) 16 | return ''.join(password) -------------------------------------------------------------------------------- /utils/imap_utils.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | from typing import Optional 4 | import asyncio 5 | from bs4 import BeautifulSoup 6 | from loguru import logger 7 | from imap_tools import MailBox, AND 8 | 9 | async def check_if_email_valid(imap_server: str, email: str, password: str) -> bool: 10 | logger.info(f'Account: {email} | Checking if email is valid...') 11 | try: 12 | await asyncio.to_thread(lambda: MailBox(imap_server).login(email, password)) 13 | return True 14 | except Exception as error: 15 | logger.error(f'Account: {email} | Email is invalid (IMAP): {error}') 16 | return False 17 | pass 18 | pass 19 | 20 | async def check_email_for_code(imap_server: str, email: str, password: str, max_attempts: int=8, delay_seconds: int=5) -> Optional[str]: 21 | code_pattern = '
\\s*([A-Z0-9])\\s*
' 22 | logger.info(f'Account: {email} | Checking email for code...') 23 | try: 24 | 25 | async def search_in_mailbox(): 26 | return await asyncio.to_thread(lambda: search_for_code_sync(MailBox(imap_server).login(email, password), code_pattern)) 27 | for attempt in range(max_attempts): 28 | link = await search_in_mailbox() 29 | if link: 30 | logger.success(f'Account: {email} | Code found: {link}') 31 | return link 32 | if attempt < max_attempts - 1: 33 | logger.info(f'Account: {email} | Code not found. Waiting {delay_seconds} seconds before next attempt...') 34 | await asyncio.sleep(delay_seconds) 35 | else: 36 | logger.warning(f'Account: {email} | Code not found after {max_attempts} attempts, searching in spam folder...') 37 | spam_folders = ('SPAM', 'Spam', 'spam', 'Junk', 'junk') 38 | for spam_folder in spam_folders: 39 | 40 | async def search_in_spam(): 41 | return await asyncio.to_thread(lambda: search_for_code_in_spam_sync(MailBox(imap_server).login(email, password), code_pattern, spam_folder)) 42 | link = await search_in_spam() 43 | if link: 44 | return link 45 | else: 46 | logger.error(f'Account: {email} | Code not found in spam folder after multiple attempts') 47 | except Exception as error: 48 | logger.error(f'Account: {email} | Failed to check email for code: {error}') 49 | 50 | def search_for_code_sync(mailbox: MailBox, code_pattern: str) -> Optional[str]: 51 | messages = mailbox.fetch(AND(from_='noreply@gradient.network')) 52 | for msg in messages: 53 | body = msg.text or msg.html 54 | if body: 55 | match = re.search(code_pattern, body) 56 | if match: 57 | soup = BeautifulSoup(body, 'html.parser') 58 | code_divs = soup.find_all('div', class_='pDiv') 59 | code = ''.join((div.text.strip() for div in code_divs if div.text.strip())) 60 | if len(code) == 6: 61 | return code 62 | else: 63 | return None 64 | 65 | def search_for_code_in_spam_sync(mailbox: MailBox, link_pattern: str, spam_folder: str) -> Optional[str]: 66 | if mailbox.folder.exists(spam_folder): 67 | mailbox.folder.set(spam_folder) 68 | return search_for_code_sync(mailbox, link_pattern) -------------------------------------------------------------------------------- /utils/load_config.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import yaml 5 | from sys import exit 6 | from itertools import cycle 7 | from loguru import logger 8 | from models import Config, Account 9 | from better_proxy import Proxy 10 | CONFIG_PATH = os.path.join(os.getcwd(), 'config') 11 | CONFIG_DATA_PATH = os.path.join(CONFIG_PATH, 'data') 12 | CONFIG_PARAMS = os.path.join(CONFIG_PATH, 'settings.yaml') 13 | REQUIRED_DATA_FILES = ('accounts.txt', 'proxies.txt') 14 | REQUIRED_PARAMS_FIELDS = ('threads', 'capsolver_api_key', 'delay_before_start', 'imap_settings', 'invite_code') 15 | 16 | def process_exit(): 17 | input('Press Enter to exit...') 18 | exit(1) 19 | pass 20 | 21 | def read_file(file_path: str, check_empty: bool=True, is_yaml: bool=False) -> list[str] | dict: 22 | if not os.path.exists(file_path): 23 | logger.error(f'File not found: {file_path}') 24 | exit(1) 25 | if check_empty and os.stat(file_path).st_size == 0: 26 | logger.error(f'File is empty: {file_path}') 27 | exit(1) 28 | if is_yaml: 29 | with open(file_path, 'r', encoding='utf-8') as file: 30 | data = yaml.safe_load(file) 31 | return data 32 | with open(file_path, 'r', encoding='utf-8') as file: 33 | data = file.readlines() 34 | return [line.strip() for line in data] 35 | 36 | def get_params() -> dict: 37 | data = read_file(CONFIG_PARAMS, is_yaml=True) 38 | for field in REQUIRED_PARAMS_FIELDS: 39 | if field not in data: 40 | logger.error(f"Field '{field}' is missing in config2 file") 41 | process_exit() 42 | return data 43 | 44 | def get_proxies() -> list[Proxy]: 45 | try: 46 | proxies = read_file(os.path.join(CONFIG_DATA_PATH, 'proxies.txt'), check_empty=False) 47 | if proxies: 48 | return [Proxy.from_str(line) for line in proxies] 49 | return [] 50 | except Exception as exc: 51 | logger.error(f'Failed to parse proxy: {exc}') 52 | process_exit() 53 | 54 | def get_accounts_to_register(): 55 | proxies = get_proxies() 56 | proxy_cycle = cycle(proxies) if proxies else None 57 | accounts = read_file(os.path.join(CONFIG_DATA_PATH, 'register.txt'), check_empty=False) 58 | for account in accounts: 59 | try: 60 | email, password = account.split(':') 61 | yield Account(email=email, password=password, proxy=next(proxy_cycle) if proxy_cycle else None) 62 | except ValueError: 63 | logger.error(f'Failed to parse account: {account}') 64 | exit(1) 65 | 66 | def get_accounts_to_farm(): 67 | proxies = get_proxies() 68 | proxy_cycle = cycle(proxies) if proxies else None 69 | accounts = read_file(os.path.join(CONFIG_DATA_PATH, 'farm.txt'), check_empty=False) 70 | for account in accounts: 71 | try: 72 | email, password = account.split(':') 73 | yield Account(email=email, password=password, proxy=next(proxy_cycle) if proxy_cycle else None) 74 | except ValueError: 75 | logger.error(f'Failed to parse account: {account}') 76 | process_exit() 77 | 78 | def validate_domains(accounts: list[Account], domains: dict): 79 | for account in accounts: 80 | try: 81 | domain = account.email.split('@')[1] 82 | if domain not in domains: 83 | logger.error(f"Domain '{domain}' is not supported, please add it to the config2 file") 84 | process_exit() 85 | imap_server = domains[domain] 86 | account.imap_server = imap_server 87 | except IndexError: 88 | logger.error(f'Failed to parse account: {account.email}') 89 | process_exit() 90 | return accounts 91 | 92 | def load_config() -> Config: 93 | try: 94 | reg_accounts = list(get_accounts_to_register()) 95 | farm_accounts = list(get_accounts_to_farm()) 96 | if not reg_accounts and (not farm_accounts): 97 | logger.error('No accounts found in data files') 98 | process_exit() 99 | config = Config(**get_params(), accounts_to_farm=farm_accounts, accounts_to_register=reg_accounts) 100 | if reg_accounts: 101 | accounts = validate_domains(reg_accounts, config.imap_settings) 102 | config.accounts_to_register = accounts 103 | return config 104 | except Exception as exc: 105 | logger.error(f'Failed to load config: {exc}') 106 | process_exit() -------------------------------------------------------------------------------- /utils/messages_generator.py: -------------------------------------------------------------------------------- 1 | 2 | import base64 3 | import json 4 | import time 5 | 6 | class MQTTMessageGenerator: 7 | 8 | def __init__(self, client_id, username, password): 9 | self.client_id = client_id 10 | self.username = username 11 | self.password = password 12 | 13 | @staticmethod 14 | def _replace_in_bytes(message, old_value, new_value): 15 | return message.replace(old_value.encode(), new_value.encode()) 16 | 17 | @staticmethod 18 | def decode_message(encoded_message): 19 | return base64.b64decode(encoded_message) 20 | 21 | def generate_task_message(self) -> bytes: 22 | original_data = 'giKXVgAAHGNsaWVudC90YXNrL1laNUdYMDY2VFZPVjQzUUIAoiGXVwAAHGNsaWVudC90YXNrL1laNUdYMDY2VFZPVjQzUUKCIpdYAAAcY2xpZW50L3Rhc2svWVo1R1gwNjZUVk9WNDNRQgA=' 23 | message = bytearray(base64.b64decode(original_data)) 24 | message = self._replace_in_bytes(message, 'YZ5GX066TVOV43QB', self.client_id) 25 | return message 26 | 27 | def generate_login_message(self) -> bytes: 28 | message_template = b"\x10\x82\x01\x00\x04MQTT\x05\xc2\x00<\x05'\x00\x10\x00\x00\x00\x10" + self.client_id.encode('utf-8') + b'\x00\x1c' + self.username.encode('utf-8') + b'\x00@' + self.password.encode('utf-8') 29 | return message_template 30 | 31 | def generate_ping_message(self) -> bytes: 32 | original_data = 'MpUBAB5jbGllbnQvb25saW5lL1laNUdYMDY2VFZPVjQzUUKXWQB7InR5cGUiOiJvbmxpbmUiLCJjbGllbnRpZCI6IllaNUdYMDY2VFZPVjQzUUIiLCJhY2NvdW50IjoiS2ROQzNPVzdOdWFvNG1LSVliQWpkNGFRNFprMSIsInRpbWVzdGFtcCI6MTcyNjc0NzQ2MTA5M30=' 33 | message = bytearray(base64.b64decode(original_data)) 34 | message = self._replace_in_bytes(message, 'YZ5GX066TVOV43QB', self.client_id) 35 | message = self._replace_in_bytes(message, 'KdNC3OW7Nuao4mKIYbAjd4aQ4Zk1', self.username) 36 | message = self._replace_in_bytes(message, '172674746s1093', str(int(time.time()) * 1000)) 37 | return message 38 | 39 | @staticmethod 40 | def generate_clear_message() -> bytes: 41 | return base64.b64decode('wAA=') 42 | 43 | def analyze_differences(self, generated, original): 44 | gen = self.decode_message(generated) 45 | orig = self.decode_message(original) 46 | differences = [] 47 | for i, (g, o) in enumerate(zip(gen, orig)): 48 | if g != o: 49 | differences.append(f'Position {i}: Generated {g:02x}, Original {o:02x}') 50 | return differences --------------------------------------------------------------------------------