x
This commit is contained in:
11
services/__init__.py
Normal file
11
services/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .email_manager import EmailManager
|
||||
from .fetch_manager import FetchManager
|
||||
from .proxy_pool import ProxyPool
|
||||
from .token_pool import TokenPool
|
||||
|
||||
__all__ = [
|
||||
'FetchManager',
|
||||
'ProxyPool',
|
||||
'TokenPool',
|
||||
'EmailManager'
|
||||
]
|
||||
93
services/capsolver.py
Normal file
93
services/capsolver.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
from typing import Optional
|
||||
import time
|
||||
|
||||
class Capsolver:
|
||||
def __init__(self, api_key: str, website_url: str, website_key: str):
|
||||
self.api_key = api_key
|
||||
self.website_url = website_url
|
||||
self.website_key = website_key
|
||||
self.base_url = "https://api.capsolver.com"
|
||||
|
||||
async def create_task(self) -> Optional[str]:
|
||||
"""创建验证码任务"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
payload = {
|
||||
"clientKey": self.api_key,
|
||||
"task": {
|
||||
"type": "AntiTurnstileTaskProxyLess",
|
||||
"websiteURL": self.website_url,
|
||||
"websiteKey": self.website_key,
|
||||
}
|
||||
}
|
||||
|
||||
async with session.post(f"{self.base_url}/createTask", json=payload) as resp:
|
||||
result = await resp.json()
|
||||
if result.get("errorId") > 0:
|
||||
logger.error(f"创建任务失败: {result.get('errorDescription')}")
|
||||
return None
|
||||
return result.get("taskId")
|
||||
|
||||
async def get_task_result(self, task_id: str) -> Optional[dict]:
|
||||
"""获取任务结果"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
payload = {
|
||||
"clientKey": self.api_key,
|
||||
"taskId": task_id
|
||||
}
|
||||
|
||||
async with session.post(f"{self.base_url}/getTaskResult", json=payload) as resp:
|
||||
result = await resp.json()
|
||||
if result.get("errorId") > 0:
|
||||
logger.error(f"获取结果失败: {result.get('errorDescription')}")
|
||||
return None
|
||||
|
||||
if result.get("status") == "ready":
|
||||
return result.get("solution", {})
|
||||
return None
|
||||
|
||||
async def solve_turnstile(self) -> Optional[str]:
|
||||
"""
|
||||
解决 Turnstile 验证码
|
||||
"""
|
||||
task_id = await self.create_task()
|
||||
if not task_id:
|
||||
raise Exception("创建验证码任务失败")
|
||||
|
||||
# 增加重试次数限制和超时时间控制
|
||||
max_retries = 5 # 减少最大重试次数
|
||||
retry_delay = 2 # 设置重试间隔为2秒
|
||||
timeout = 15 # 设置总超时时间为15秒
|
||||
|
||||
start_time = time.time()
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
logger.debug(f"第 {attempt} 次尝试获取验证码结果")
|
||||
result = await self.get_task_result(task_id)
|
||||
|
||||
if result and "token" in result:
|
||||
token = result["token"]
|
||||
logger.success(f"成功获取验证码 token: {token[:40]}...")
|
||||
return token
|
||||
|
||||
# 检查是否超时
|
||||
if time.time() - start_time > timeout:
|
||||
logger.error("验证码请求总时间超过15秒")
|
||||
break
|
||||
|
||||
await asyncio.sleep(retry_delay)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取验证码结果失败: {str(e)}")
|
||||
if attempt == max_retries:
|
||||
raise
|
||||
|
||||
if time.time() - start_time > timeout:
|
||||
logger.error("验证码请求总时间超过15秒")
|
||||
break
|
||||
|
||||
await asyncio.sleep(retry_delay)
|
||||
|
||||
raise Exception("验证码解决失败: 达到最大重试次数或超时")
|
||||
245
services/email_manager.py
Normal file
245
services/email_manager.py
Normal file
@@ -0,0 +1,245 @@
|
||||
import asyncio
|
||||
import email
|
||||
from dataclasses import dataclass
|
||||
from email.header import decode_header, make_header
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
from core.database import DatabaseManager
|
||||
from core.exceptions import EmailError
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmailAccount:
|
||||
id: int
|
||||
email: str
|
||||
password: str # 这里实际上是 refresh_token
|
||||
client_id: str
|
||||
refresh_token: str
|
||||
in_use: bool = False
|
||||
cursor_password: Optional[str] = None
|
||||
cursor_cookie: Optional[str] = None
|
||||
sold: bool = False
|
||||
status: str = 'pending' # 新增状态字段: pending, unavailable, success
|
||||
|
||||
|
||||
class EmailManager:
|
||||
def __init__(self, config: Config, db_manager: DatabaseManager):
|
||||
self.config = config
|
||||
self.db = db_manager
|
||||
self.verification_subjects = [
|
||||
"Verify your email address",
|
||||
"Complete code challenge",
|
||||
]
|
||||
|
||||
async def batch_get_accounts(self, num: int) -> List[EmailAccount]:
|
||||
"""批量获取未使用的邮箱账号"""
|
||||
logger.info(f"尝试获取 {num} 个未使用的邮箱账号")
|
||||
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET in_use = 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id IN (
|
||||
SELECT id FROM email_accounts
|
||||
WHERE in_use = 0 AND sold = 0 AND status = 'pending'
|
||||
LIMIT ?
|
||||
)
|
||||
RETURNING id, email, password, client_id, refresh_token
|
||||
'''
|
||||
|
||||
results = await self.db.fetch_all(query, (num,))
|
||||
logger.debug(f"实际获取到 {len(results)} 个账号")
|
||||
return [
|
||||
EmailAccount(
|
||||
id=row[0],
|
||||
email=row[1],
|
||||
password=row[2],
|
||||
client_id=row[3],
|
||||
refresh_token=row[4],
|
||||
in_use=True
|
||||
)
|
||||
for row in results
|
||||
]
|
||||
|
||||
async def update_account_status(self, account_id: int, status: str):
|
||||
"""更新账号状态"""
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET
|
||||
status = ?,
|
||||
in_use = 0,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
'''
|
||||
await self.db.execute(query, (status, account_id))
|
||||
|
||||
async def update_account(self, account_id: int, cursor_password: str, cursor_cookie: str, cursor_token: str):
|
||||
"""更新账号信息"""
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET
|
||||
cursor_password = ?,
|
||||
cursor_cookie = ?,
|
||||
cursor_token = ?,
|
||||
in_use = 0,
|
||||
sold = 1,
|
||||
status = 'success',
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
'''
|
||||
await self.db.execute(query, (cursor_password, cursor_cookie, cursor_token, account_id))
|
||||
|
||||
async def release_account(self, account_id: int):
|
||||
"""释放账号"""
|
||||
query = '''
|
||||
UPDATE email_accounts
|
||||
SET in_use = 0, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
'''
|
||||
await self.db.execute(query, (account_id,))
|
||||
|
||||
async def _get_access_token(self, client_id: str, refresh_token: str) -> str:
|
||||
"""获取微软 access token"""
|
||||
logger.debug(f"开始获取 access token - client_id: {client_id}")
|
||||
|
||||
url = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'
|
||||
data = {
|
||||
'client_id': client_id,
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refresh_token,
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, data=data) as response:
|
||||
result = await response.json()
|
||||
|
||||
if 'error' in result:
|
||||
error = result.get('error')
|
||||
logger.error(f"获取 access token 失败: {error}")
|
||||
raise EmailError(f"Failed to get access token: {error}")
|
||||
|
||||
access_token = result['access_token']
|
||||
logger.debug("成功获取 access token")
|
||||
return access_token
|
||||
|
||||
async def get_verification_code(self, email: str, refresh_token: str, client_id: str) -> str:
|
||||
"""获取验证码"""
|
||||
logger.info(f"开始获取邮箱验证码 - {email}")
|
||||
try:
|
||||
# 1. 获取 access token
|
||||
access_token = await self._get_access_token(client_id, refresh_token)
|
||||
logger.debug(f"[{email}] 获取 access token 成功")
|
||||
|
||||
# 2. 构建认证字符串
|
||||
auth_string = f"user={email}\1auth=Bearer {access_token}\1\1"
|
||||
logger.debug(f"[{email}] 认证字符串构建完成")
|
||||
|
||||
# 3. 连接邮箱
|
||||
import imaplib
|
||||
mail = imaplib.IMAP4_SSL('outlook.live.com')
|
||||
mail.authenticate('XOAUTH2', lambda x: auth_string)
|
||||
mail.select('inbox')
|
||||
logger.debug(f"[{email}] 邮箱连接成功")
|
||||
|
||||
# 4. 等待并获取验证码邮件
|
||||
for i in range(15):
|
||||
logger.debug(f"[{email}] 第 {i + 1} 次尝试获取验证码")
|
||||
|
||||
# 搜索来自 no-reply@cursor.sh 的最新邮件
|
||||
result, data = mail.search(None, '(FROM "no-reply@cursor.sh")')
|
||||
if result != "OK" or not data[0]:
|
||||
logger.debug(f"[{email}] 未找到来自 cursor 的邮件,等待1秒后重试")
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
mail_ids = data[0].split()
|
||||
if not mail_ids:
|
||||
logger.debug(f"[{email}] 邮件ID列表为空,等待1秒后重试")
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
# 获取最新的3封邮件
|
||||
last_mail_ids = sorted(mail_ids, reverse=True)[:3]
|
||||
|
||||
for mail_id in last_mail_ids:
|
||||
result, msg_data = mail.fetch(mail_id, "(RFC822)")
|
||||
if result != 'OK':
|
||||
logger.warning(f"[{email}] 获取邮件内容失败: {result}")
|
||||
continue
|
||||
|
||||
# 确保 msg_data 不为空且格式正确
|
||||
if not msg_data or not msg_data[0] or len(msg_data[0]) < 2:
|
||||
logger.warning(f"[{email}] 邮件数据格式不正确")
|
||||
continue
|
||||
|
||||
# 正确导入 email 模块
|
||||
from email import message_from_bytes
|
||||
email_message = message_from_bytes(msg_data[0][1])
|
||||
|
||||
# 检查发件人
|
||||
from_addr = str(make_header(decode_header(email_message['From'])))
|
||||
if 'no-reply@cursor.sh' not in from_addr:
|
||||
logger.debug(f"[{email}] 跳过非 Cursor 邮件,发件人: {from_addr}")
|
||||
continue
|
||||
|
||||
# 检查主题
|
||||
subject = str(make_header(decode_header(email_message['SUBJECT'])))
|
||||
if not any(verify_subject in subject for verify_subject in self.verification_subjects):
|
||||
logger.debug(f"[{email}] 跳过非验证码邮件,主题: {subject}")
|
||||
continue
|
||||
|
||||
code = self._extract_code_from_email(email_message)
|
||||
if code:
|
||||
logger.debug(f"[{email}] 成功获取验证码: {code}")
|
||||
mail.close()
|
||||
mail.logout()
|
||||
return code
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
logger.error(f"[{email}] 验证码邮件未收到")
|
||||
raise EmailError("Verification code not received")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{email}] 获取验证码失败: {str(e)}")
|
||||
raise EmailError(f"Failed to get verification code: {str(e)}")
|
||||
|
||||
def _extract_code_from_email(self, email_message) -> Optional[str]:
|
||||
"""从邮件内容中提取验证码"""
|
||||
try:
|
||||
# 获取邮件内容
|
||||
if email_message.is_multipart():
|
||||
for part in email_message.walk():
|
||||
if part.get_content_type() == "text/html":
|
||||
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
|
||||
break
|
||||
else:
|
||||
body = email_message.get_payload(decode=True).decode('utf-8', errors='ignore')
|
||||
|
||||
# 提取6位数字验证码
|
||||
import re
|
||||
|
||||
# 在HTML中查找包含6位数字的div
|
||||
match = re.search(r'<div[^>]*>(\d{6})</div>', body)
|
||||
if match:
|
||||
code = match.group(1)
|
||||
logger.debug(f"从HTML中提取到验证码: {code}")
|
||||
return code
|
||||
|
||||
# 备用方案:搜索任何6位数字
|
||||
match = re.search(r'\b\d{6}\b', body)
|
||||
if match:
|
||||
code = match.group(0)
|
||||
logger.debug(f"从文本中提取到验证码: {code}")
|
||||
return code
|
||||
|
||||
logger.warning(f"[{email}] 未能从邮件中提取到验证码")
|
||||
logger.debug(f"[{email}] 邮件内容预览: " + body[:200])
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{email}] 提取验证码失败: {str(e)}")
|
||||
return None
|
||||
46
services/fetch_manager.py
Normal file
46
services/fetch_manager.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import asyncio
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
|
||||
from .fetch_service import FetchService
|
||||
|
||||
|
||||
class FetchManager:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
self.fetch_service = FetchService()
|
||||
self.semaphore = asyncio.Semaphore(config.global_config.max_concurrency)
|
||||
|
||||
async def request(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
proxy: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
使用信号量控制并发的请求方法
|
||||
"""
|
||||
async with self.semaphore:
|
||||
for _ in range(self.config.global_config.retry_times):
|
||||
try:
|
||||
response = await self.fetch_service.request(
|
||||
method=method,
|
||||
url=url,
|
||||
proxy=proxy,
|
||||
timeout=self.config.global_config.timeout,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
if 'error' not in response:
|
||||
return response
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"请求超时,正在重试: {url}")
|
||||
continue
|
||||
|
||||
logger.error(f"达到最大重试次数: {url}")
|
||||
return {'error': 'Max retries exceeded'}
|
||||
80
services/fetch_service.py
Normal file
80
services/fetch_service.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
from curl_cffi.requests import AsyncSession
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class FetchService:
|
||||
def __init__(self):
|
||||
self.default_headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd"
|
||||
}
|
||||
|
||||
async def request(
|
||||
self,
|
||||
method: str,
|
||||
url: str,
|
||||
*,
|
||||
headers: Optional[Dict] = None,
|
||||
params: Optional[Dict] = None,
|
||||
data: Optional[Union[Dict, str]] = None,
|
||||
json: Optional[Dict] = None,
|
||||
cookies: Optional[Dict] = None,
|
||||
proxy: Optional[str] = None,
|
||||
impersonate: str = "chrome124",
|
||||
**kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
通用请求方法
|
||||
|
||||
Args:
|
||||
method: 请求方法 (GET, POST 等)
|
||||
url: 请求URL
|
||||
headers: 请求头
|
||||
params: URL参数
|
||||
data: 表单数据
|
||||
json: JSON数据
|
||||
cookies: Cookie
|
||||
proxy: 代理地址
|
||||
impersonate: 浏览器仿真类型
|
||||
**kwargs: 其他curl_cffi支持的参数
|
||||
|
||||
Returns:
|
||||
Dict 包含响应信息
|
||||
"""
|
||||
# 合并默认headers
|
||||
request_headers = self.default_headers.copy()
|
||||
if headers:
|
||||
request_headers.update(headers)
|
||||
|
||||
try:
|
||||
async with AsyncSession(impersonate=impersonate) as session:
|
||||
response = await session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
headers=request_headers,
|
||||
params=params,
|
||||
data=data,
|
||||
json=json,
|
||||
cookies=cookies,
|
||||
proxies={'http': proxy, 'https': proxy} if proxy else None,
|
||||
verify=False,
|
||||
quote=False,
|
||||
stream=True,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return {
|
||||
'status': response.status_code,
|
||||
'headers': dict(response.headers),
|
||||
'cookies': dict(response.cookies),
|
||||
'body': await response.acontent(),
|
||||
'raw_response': response
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"请求失败: {str(e)}")
|
||||
return {'error': str(e)}
|
||||
38
services/proxy_pool.py
Normal file
38
services/proxy_pool.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from typing import List
|
||||
|
||||
from core.config import Config
|
||||
from core.exceptions import ProxyFetchError
|
||||
|
||||
from .fetch_manager import FetchManager
|
||||
|
||||
|
||||
class ProxyPool:
|
||||
def __init__(self, config: Config, fetch_manager: FetchManager):
|
||||
self.config = config
|
||||
self.fetch_manager = fetch_manager
|
||||
|
||||
async def batch_get(self, num: int) -> List[str]:
|
||||
"""获取num个代理"""
|
||||
# 临时代理
|
||||
return ['http://127.0.0.1:3057'] * num
|
||||
|
||||
try:
|
||||
response = await self.fetch_manager.request(
|
||||
'GET',
|
||||
self.config.proxy_config.api_url.format(num=num)
|
||||
)
|
||||
|
||||
if 'error' in response:
|
||||
raise ProxyFetchError(response['error'])
|
||||
|
||||
# 这里需要根据实际的代理API返回格式进行解析
|
||||
proxies = self._parse_proxies(response['body'])
|
||||
return proxies[:num]
|
||||
|
||||
except Exception as e:
|
||||
raise ProxyFetchError(f"Failed to fetch proxies: {str(e)}")
|
||||
|
||||
def _parse_proxies(self, response_body: str) -> List[str]:
|
||||
"""解析代理API返回的数据"""
|
||||
# 需要根据实际API返回格式实现
|
||||
...
|
||||
96
services/token_pool.py
Normal file
96
services/token_pool.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import asyncio
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core.config import Config
|
||||
from core.exceptions import TokenGenerationError
|
||||
from services.yescaptcha import TurnstileConfig, YesCaptcha
|
||||
from services.capsolver import Capsolver
|
||||
|
||||
|
||||
class TokenPool:
|
||||
def __init__(self, config: Config):
|
||||
self.config = config
|
||||
|
||||
if config.captcha_config.provider == "capsolver":
|
||||
self.solver = Capsolver(
|
||||
api_key=config.captcha_config.capsolver.api_key,
|
||||
website_url=config.captcha_config.capsolver.website_url,
|
||||
website_key=config.captcha_config.capsolver.website_key
|
||||
)
|
||||
else:
|
||||
self.turnstile_config = TurnstileConfig(
|
||||
client_key=config.captcha_config.yescaptcha.client_key,
|
||||
website_url=config.captcha_config.yescaptcha.website_url,
|
||||
website_key=config.captcha_config.yescaptcha.website_key,
|
||||
use_cn_server=config.captcha_config.yescaptcha.use_cn_server
|
||||
)
|
||||
self.solver = YesCaptcha(self.turnstile_config)
|
||||
|
||||
async def _get_token(self) -> str:
|
||||
"""获取单个token"""
|
||||
try:
|
||||
if isinstance(self.solver, Capsolver):
|
||||
# Capsolver 是异步的,直接调用
|
||||
token = await self.solver.solve_turnstile()
|
||||
else:
|
||||
# YesCaptcha 是同步的,需要转换
|
||||
token = await asyncio.to_thread(self.solver.solve_turnstile)
|
||||
|
||||
if not token:
|
||||
raise TokenGenerationError("Failed to get token")
|
||||
return token
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取 token 失败: {str(e)}")
|
||||
raise TokenGenerationError(f"Failed to get token: {str(e)}")
|
||||
|
||||
async def get_token_pair(self) -> Tuple[str, str]:
|
||||
"""获取一对token"""
|
||||
token1 = await self._get_token()
|
||||
token2 = await self._get_token()
|
||||
return token1, token2
|
||||
|
||||
async def batch_generate(self, num: int) -> List[Tuple[str, str]]:
|
||||
"""批量生成token对
|
||||
|
||||
Args:
|
||||
num: 需要的token对数量
|
||||
|
||||
Returns:
|
||||
List[Tuple[str, str]]: token对列表,每个元素是(token1, token2)
|
||||
"""
|
||||
logger.info(f"开始批量生成 {num} 对 token")
|
||||
|
||||
# 创建所有token获取任务
|
||||
tasks = []
|
||||
for _ in range(num * 2): # 每对需要两个token
|
||||
tasks.append(self._get_token())
|
||||
|
||||
# 并发执行所有任务
|
||||
try:
|
||||
tokens = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 过滤出成功的token(仅保留字符串类型)
|
||||
valid_tokens = [
|
||||
token for token in tokens
|
||||
if isinstance(token, str) and token.startswith('0.')
|
||||
]
|
||||
|
||||
# 将token分组为对
|
||||
token_pairs = []
|
||||
for i in range(0, num * 2, 2):
|
||||
try:
|
||||
pair = (valid_tokens[i], valid_tokens[i+1])
|
||||
token_pairs.append(pair)
|
||||
except IndexError:
|
||||
logger.error(f"生成token对时索引越界,i={i}, tokens数量={len(valid_tokens)}")
|
||||
break
|
||||
|
||||
logger.success(f"成功生成 {len(token_pairs)} 对 token")
|
||||
return token_pairs
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"批量生成 token 失败: {str(e)}")
|
||||
return []
|
||||
32
services/uuid.py
Normal file
32
services/uuid.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
|
||||
class ULID:
|
||||
def __init__(self):
|
||||
# 定义字符集,使用Crockford's Base32字符集
|
||||
self.encoding = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
|
||||
def generate(self) -> str:
|
||||
# 获取当前时间戳(毫秒)
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
# 生成随机数部分
|
||||
randomness = random.getrandbits(80) # 80位随机数
|
||||
|
||||
# 转换时间戳为base32字符串(10个字符)
|
||||
time_chars = []
|
||||
for _ in range(10):
|
||||
timestamp, mod = divmod(timestamp, 32)
|
||||
time_chars.append(self.encoding[mod])
|
||||
time_chars.reverse()
|
||||
|
||||
# 转换随机数为base32字符串(16个字符)
|
||||
random_chars = []
|
||||
for _ in range(16):
|
||||
randomness, mod = divmod(randomness, 32)
|
||||
random_chars.append(self.encoding[mod])
|
||||
random_chars.reverse()
|
||||
|
||||
# 组合最终结果
|
||||
return ''.join(time_chars + random_chars)
|
||||
117
services/yescaptcha.py
Normal file
117
services/yescaptcha.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional
|
||||
|
||||
import requests
|
||||
from loguru import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnstileConfig:
|
||||
client_key: str
|
||||
website_url: str
|
||||
website_key: str
|
||||
use_cn_server: bool = True
|
||||
|
||||
|
||||
class YesCaptcha:
|
||||
API_URL_GLOBAL = "https://api.yescaptcha.com"
|
||||
API_URL_CN = "https://cn.yescaptcha.com"
|
||||
|
||||
def __init__(self, config: TurnstileConfig):
|
||||
self.config = config
|
||||
self.base_url = self.API_URL_CN if config.use_cn_server else self.API_URL_GLOBAL
|
||||
logger.debug(f"YesCaptcha 初始化 - 使用{'国内' if config.use_cn_server else '国际'}服务器")
|
||||
|
||||
def create_task(self, task_type: str = "TurnstileTaskProxyless") -> Dict:
|
||||
"""
|
||||
Create a new Turnstile solving task
|
||||
|
||||
Args:
|
||||
task_type: Either "TurnstileTaskProxyless" (25 points) or "TurnstileTaskProxylessM1" (30 points)
|
||||
|
||||
Returns:
|
||||
Dict containing task ID if successful
|
||||
"""
|
||||
url = f"{self.base_url}/createTask"
|
||||
logger.debug(f"创建验证任务 - 类型: {task_type}")
|
||||
|
||||
payload = {
|
||||
"clientKey": self.config.client_key,
|
||||
"task": {
|
||||
"type": task_type,
|
||||
"websiteURL": self.config.website_url,
|
||||
"websiteKey": self.config.website_key
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errorId", 1) != 0:
|
||||
logger.error(f"创建任务失败: {result.get('errorDescription')}")
|
||||
else:
|
||||
logger.debug(f"创建任务成功 - TaskID: {result.get('taskId')}")
|
||||
|
||||
return result
|
||||
|
||||
def get_task_result(self, task_id: str) -> Dict:
|
||||
"""
|
||||
Get the result of a task
|
||||
|
||||
Args:
|
||||
task_id: Task ID from create_task
|
||||
|
||||
Returns:
|
||||
Dict containing task result if successful
|
||||
"""
|
||||
url = f"{self.base_url}/getTaskResult"
|
||||
logger.debug(f"获取任务结果 - TaskID: {task_id}")
|
||||
|
||||
payload = {
|
||||
"clientKey": self.config.client_key,
|
||||
"taskId": task_id
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errorId", 1) != 0:
|
||||
logger.error(f"获取结果失败: {result.get('errorDescription')}")
|
||||
elif result.get("status") == "ready":
|
||||
logger.debug("成功获取到结果")
|
||||
|
||||
return result
|
||||
|
||||
def solve_turnstile(self, max_attempts: int = 60) -> Optional[str]:
|
||||
"""
|
||||
Complete turnstile solving process
|
||||
|
||||
Args:
|
||||
max_attempts: Maximum number of attempts to get result
|
||||
|
||||
Returns:
|
||||
Token string if successful, None otherwise
|
||||
"""
|
||||
# 创建任务
|
||||
create_result = self.create_task()
|
||||
if create_result.get("errorId", 1) != 0:
|
||||
return None
|
||||
|
||||
task_id = create_result.get("taskId")
|
||||
if not task_id:
|
||||
return None
|
||||
|
||||
# 轮询获取结果
|
||||
for _ in range(max_attempts):
|
||||
result = self.get_task_result(task_id)
|
||||
|
||||
if result.get("status") == "ready":
|
||||
return result.get("solution", {}).get("token")
|
||||
|
||||
if result.get("errorId", 1) != 0:
|
||||
return None
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user