|
|
|
|
|
|
|
|
"""
|
|
|
Warp账号注册脚本
|
|
|
使用Outlook邮箱API和动态IP代理进行账号注册
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
import copy
|
|
|
import email as email_lib
|
|
|
import html
|
|
|
import imaplib
|
|
|
import logging
|
|
|
import random
|
|
|
import re
|
|
|
import secrets
|
|
|
import ssl
|
|
|
import time
|
|
|
import uuid
|
|
|
from datetime import datetime
|
|
|
from typing import Dict, Any, Optional
|
|
|
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
|
|
|
|
import aiosqlite
|
|
|
import httpx
|
|
|
from fake_useragent import UserAgent
|
|
|
|
|
|
|
|
|
import config
|
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
level=config.LOG_LEVEL,
|
|
|
format=config.LOG_FORMAT
|
|
|
)
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
ua = UserAgent()
|
|
|
|
|
|
|
|
|
|
|
|
class TempMailAPIClient:
|
|
|
"""临时邮箱API客户端"""
|
|
|
|
|
|
def __init__(self):
|
|
|
self.base_url = config.TEMP_MAIL_BASE_URL
|
|
|
self.client = None
|
|
|
self.current_email = None
|
|
|
|
|
|
async def __aenter__(self):
|
|
|
await self._ensure_client()
|
|
|
return self
|
|
|
|
|
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
|
await self.close()
|
|
|
|
|
|
async def _ensure_client(self):
|
|
|
if self.client is None or self.client.is_closed:
|
|
|
self.client = httpx.AsyncClient(
|
|
|
timeout=httpx.Timeout(30.0),
|
|
|
headers={
|
|
|
'User-Agent': ua.random,
|
|
|
'Accept': 'application/json'
|
|
|
},
|
|
|
proxy=None,
|
|
|
trust_env=False
|
|
|
)
|
|
|
|
|
|
async def close(self):
|
|
|
if self.client and not self.client.is_closed:
|
|
|
await self.client.aclose()
|
|
|
self.client = None
|
|
|
|
|
|
async def generate_email(self) -> Dict[str, Any]:
|
|
|
"""生成临时邮箱"""
|
|
|
await self._ensure_client()
|
|
|
|
|
|
url = f"{self.base_url}/generate-email"
|
|
|
|
|
|
try:
|
|
|
response = await self.client.get(url)
|
|
|
response.raise_for_status()
|
|
|
result = response.json()
|
|
|
self.current_email = result.get('email')
|
|
|
logger.info(f"✅ 生成临时邮箱: {self.current_email}")
|
|
|
return {
|
|
|
"success": True,
|
|
|
"email": self.current_email
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"生成邮箱失败: {type(e).__name__}: {e}", exc_info=True)
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
async def get_emails(self, email: str = None) -> Dict[str, Any]:
|
|
|
"""获取邮件列表"""
|
|
|
await self._ensure_client()
|
|
|
|
|
|
target_email = email or self.current_email
|
|
|
if not target_email:
|
|
|
return {"success": False, "error": "未指定邮箱地址"}
|
|
|
|
|
|
url = f"{self.base_url}/get-emails"
|
|
|
params = {'email': target_email}
|
|
|
|
|
|
try:
|
|
|
response = await self.client.get(url, params=params)
|
|
|
response.raise_for_status()
|
|
|
result = response.json()
|
|
|
emails = result.get('emails', [])
|
|
|
logger.info(f"获取到 {len(emails)} 封邮件")
|
|
|
return {
|
|
|
"success": True,
|
|
|
"emails": emails
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"获取邮件失败: {type(e).__name__}: {e}", exc_info=True)
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncProxyManager:
|
|
|
"""异步代理管理器"""
|
|
|
|
|
|
def __init__(self):
|
|
|
self.used_identifiers = {}
|
|
|
self.lock = asyncio.Lock()
|
|
|
|
|
|
async def cleanup_expired_identifiers(self):
|
|
|
"""清理过期的IP标识"""
|
|
|
current_time = datetime.now()
|
|
|
async with self.lock:
|
|
|
expired_keys = [k for k, v in self.used_identifiers.items() if v < current_time]
|
|
|
for key in expired_keys:
|
|
|
del self.used_identifiers[key]
|
|
|
|
|
|
async def get_proxy(self) -> Optional[str]:
|
|
|
"""获取代理IP"""
|
|
|
return config.PROXY_URL
|
|
|
|
|
|
def format_proxy_for_httpx(self, proxy_str: str) -> Optional[str]:
|
|
|
"""格式化代理为httpx格式"""
|
|
|
if not proxy_str:
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
if proxy_str.startswith(('http://', 'https://', 'socks5://', 'socks4://')):
|
|
|
return proxy_str
|
|
|
|
|
|
|
|
|
if '@' in proxy_str:
|
|
|
credentials, host_port = proxy_str.split('@')
|
|
|
user, password = credentials.split(':')
|
|
|
host, port = host_port.split(':')
|
|
|
return f"socks5://{user}:{password}@{host}:{port}"
|
|
|
else:
|
|
|
parts = proxy_str.split(':')
|
|
|
if len(parts) == 2:
|
|
|
host, port = parts
|
|
|
return f"socks5://{host}:{port}"
|
|
|
else:
|
|
|
logger.error(f"代理格式无法识别: {proxy_str}")
|
|
|
return None
|
|
|
except Exception as e:
|
|
|
logger.error(f"格式化代理失败: {e}", exc_info=True)
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncDatabaseManager:
|
|
|
"""异步数据库管理器"""
|
|
|
|
|
|
def __init__(self, db_path=config.DATABASE_PATH):
|
|
|
self.db_path = db_path
|
|
|
|
|
|
async def add_account(self, email, local_id, id_token, refresh_token,
|
|
|
status='active', proxy_info=None, user_agent=None):
|
|
|
"""添加账号"""
|
|
|
try:
|
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
|
await db.execute('''
|
|
|
INSERT INTO accounts
|
|
|
(email, local_id, id_token, refresh_token, status, proxy_info, user_agent)
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
|
''', (email, local_id, id_token, refresh_token, status, proxy_info, user_agent))
|
|
|
|
|
|
await db.commit()
|
|
|
logger.info(f"✅ 账号已保存: {email}")
|
|
|
return True
|
|
|
except aiosqlite.IntegrityError:
|
|
|
logger.warning(f"账号已存在: {email}")
|
|
|
return False
|
|
|
except Exception as e:
|
|
|
logger.error(f"保存账号失败: {e}")
|
|
|
return False
|
|
|
|
|
|
async def get_account_count(self, status='active'):
|
|
|
"""获取账号数量"""
|
|
|
async with aiosqlite.connect(self.db_path) as db:
|
|
|
cursor = await db.execute('SELECT COUNT(*) FROM accounts WHERE status = ?', (status,))
|
|
|
row = await cursor.fetchone()
|
|
|
return row[0] if row else 0
|
|
|
|
|
|
|
|
|
|
|
|
class WarpRegistrationBot:
|
|
|
"""Warp注册机器人"""
|
|
|
|
|
|
def __init__(self, db_manager: AsyncDatabaseManager, proxy_manager: AsyncProxyManager):
|
|
|
self.db_manager = db_manager
|
|
|
self.proxy_manager = proxy_manager
|
|
|
self.firebase_api_keys = config.FIREBASE_API_KEYS
|
|
|
self.current_api_key_index = 0
|
|
|
self.user_agent = ua.random
|
|
|
self.async_client = None
|
|
|
|
|
|
def get_next_api_key(self) -> str:
|
|
|
"""获取下一个Firebase API密钥"""
|
|
|
key = self.firebase_api_keys[self.current_api_key_index]
|
|
|
self.current_api_key_index = (self.current_api_key_index + 1) % len(self.firebase_api_keys)
|
|
|
return key
|
|
|
|
|
|
async def send_email_signin_request(self, email: str, proxy: str = None) -> Dict[str, Any]:
|
|
|
"""发送邮箱登录请求"""
|
|
|
api_key = self.get_next_api_key()
|
|
|
url = f"https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key={api_key}"
|
|
|
|
|
|
payload = {
|
|
|
"requestType": "EMAIL_SIGNIN",
|
|
|
"email": email,
|
|
|
"clientType": "CLIENT_TYPE_WEB",
|
|
|
"continueUrl": "https://app.warp.dev/login",
|
|
|
"canHandleCodeInApp": True
|
|
|
}
|
|
|
|
|
|
headers = {
|
|
|
"Content-Type": "application/json",
|
|
|
"User-Agent": self.user_agent,
|
|
|
"Accept": "*/*",
|
|
|
"Accept-Language": "en-US,en;q=0.9"
|
|
|
}
|
|
|
|
|
|
async with httpx.AsyncClient(
|
|
|
proxy=proxy,
|
|
|
verify=False,
|
|
|
timeout=httpx.Timeout(30.0),
|
|
|
headers=headers
|
|
|
) as client:
|
|
|
try:
|
|
|
response = await client.post(url, json=payload)
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
logger.info(f"✅ 发送登录请求成功: {email}")
|
|
|
return {
|
|
|
"success": True,
|
|
|
"response": response.json()
|
|
|
}
|
|
|
else:
|
|
|
logger.error(f"发送登录请求失败: {response.status_code}")
|
|
|
return {
|
|
|
"success": False,
|
|
|
"error": response.text
|
|
|
}
|
|
|
except (httpx.ProxyError, ssl.SSLError) as e:
|
|
|
logger.error(f"代理错误: {e}")
|
|
|
return {
|
|
|
"success": False,
|
|
|
"error": "proxy_error"
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"发送登录请求异常: {type(e).__name__}: {e}", exc_info=True)
|
|
|
return {
|
|
|
"success": False,
|
|
|
"error": str(e)
|
|
|
}
|
|
|
|
|
|
async def wait_for_verification_email(self, temp_mail_client: TempMailAPIClient, email: str,
|
|
|
timeout: int = 60) -> Optional[Dict[str, Any]]:
|
|
|
"""等待Warp验证邮件(使用TEMP_MAIL_API)"""
|
|
|
logger.info(f"📬 等待验证邮件 (超时: {timeout}秒)...")
|
|
|
await asyncio.sleep(3)
|
|
|
|
|
|
start_time = time.time()
|
|
|
check_count = 0
|
|
|
|
|
|
while time.time() - start_time < timeout:
|
|
|
check_count += 1
|
|
|
logger.info(f" 第 {check_count} 次检查...")
|
|
|
|
|
|
try:
|
|
|
result = await self._check_email_temp(temp_mail_client, email)
|
|
|
|
|
|
if result:
|
|
|
return result
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"检查邮件时出错: {e}")
|
|
|
|
|
|
await asyncio.sleep(5)
|
|
|
|
|
|
logger.error("❌ 等待验证邮件超时")
|
|
|
return None
|
|
|
|
|
|
async def _check_email_temp(self, temp_mail_client: TempMailAPIClient, email: str) -> Optional[Dict[str, Any]]:
|
|
|
"""使用TEMP_MAIL_API检查邮件"""
|
|
|
try:
|
|
|
logger.info(f" 正在检查邮箱: {email}")
|
|
|
result = await temp_mail_client.get_emails(email)
|
|
|
|
|
|
if not result.get('success'):
|
|
|
logger.warning(f"获取邮件失败: {result.get('error')}")
|
|
|
return None
|
|
|
|
|
|
emails = result.get('emails', [])
|
|
|
if not emails:
|
|
|
logger.info(" 暂无邮件")
|
|
|
return None
|
|
|
|
|
|
|
|
|
emails.sort(key=lambda x: x.get('date', ''), reverse=True)
|
|
|
|
|
|
|
|
|
for email_data in emails[:3]:
|
|
|
subject = email_data.get('subject', '')
|
|
|
if 'warp' in subject.lower():
|
|
|
|
|
|
body_content = email_data.get('htmlContent') or email_data.get('content', '')
|
|
|
verification_data = self._extract_verification_link(body_content)
|
|
|
if verification_data:
|
|
|
logger.info(f"✅ 找到验证邮件: {subject}")
|
|
|
return verification_data
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"TEMP_MAIL_API邮件检查失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
def _extract_verification_link(self, body_content: str) -> Optional[Dict[str, Any]]:
|
|
|
"""从邮件内容中提取验证链接"""
|
|
|
link_patterns = [
|
|
|
r'href=["\'](https://[^"\']*firebaseapp\.com[^"\']*)["\']',
|
|
|
r'(https://astral-field[^"\'\s<>]+)',
|
|
|
r'https://[^"\'\s<>]*__/auth/action[^"\'\s<>]*',
|
|
|
r'(https://[^\s<>]+\?.*oobCode=[^"\'\s<>]+)'
|
|
|
]
|
|
|
|
|
|
for pattern in link_patterns:
|
|
|
matches = re.findall(pattern, body_content, re.IGNORECASE)
|
|
|
if matches:
|
|
|
verification_link = html.unescape(matches[0])
|
|
|
verification_link = verification_link.replace('&', '&')
|
|
|
|
|
|
parsed = urlparse(verification_link)
|
|
|
params = parse_qs(parsed.query)
|
|
|
|
|
|
oob_code = params.get('oobCode', [None])[0]
|
|
|
if oob_code:
|
|
|
return {
|
|
|
"oob_code": oob_code,
|
|
|
"verification_link": verification_link
|
|
|
}
|
|
|
|
|
|
return None
|
|
|
|
|
|
def _check_email_sync(self, access_token: str, email: str) -> Optional[Dict[str, Any]]:
|
|
|
"""同步检查邮件(在executor中运行)"""
|
|
|
try:
|
|
|
mail = imaplib.IMAP4_SSL('outlook.office365.com')
|
|
|
auth_string = f"user={email}\x01auth=Bearer {access_token}\x01\x01"
|
|
|
mail.authenticate('XOAUTH2', lambda x: auth_string)
|
|
|
|
|
|
for folder in ["INBOX", "Junk"]:
|
|
|
try:
|
|
|
mail.select(folder)
|
|
|
|
|
|
search_criteria = [
|
|
|
'(FROM "noreply@warp.dev")',
|
|
|
'(FROM "noreply@firebase.com")',
|
|
|
'(SUBJECT "Sign in")',
|
|
|
'(SUBJECT "Warp")',
|
|
|
'(SUBJECT "verify")'
|
|
|
]
|
|
|
|
|
|
for criteria in search_criteria:
|
|
|
try:
|
|
|
status, message_ids = mail.search(None, criteria)
|
|
|
|
|
|
if status == 'OK' and message_ids[0]:
|
|
|
email_ids = message_ids[0].split()
|
|
|
|
|
|
|
|
|
for message_id in reversed(email_ids[-3:]):
|
|
|
status, msg_data = mail.fetch(message_id, '(RFC822)')
|
|
|
|
|
|
if status != 'OK':
|
|
|
continue
|
|
|
|
|
|
for response_part in msg_data:
|
|
|
if isinstance(response_part, tuple):
|
|
|
msg = email_lib.message_from_bytes(response_part[1])
|
|
|
|
|
|
|
|
|
body = self._extract_email_body(msg)
|
|
|
|
|
|
|
|
|
link_patterns = [
|
|
|
r'href=["\'](https://[^"\']*firebaseapp\.com[^"\']*)["\']',
|
|
|
r'(https://astral-field[^"\'\s<>]+)',
|
|
|
r'https://[^"\'\s<>]*__/auth/action[^"\'\s<>]*'
|
|
|
]
|
|
|
|
|
|
for pattern in link_patterns:
|
|
|
matches = re.findall(pattern, body, re.IGNORECASE)
|
|
|
if matches:
|
|
|
|
|
|
verification_link = html.unescape(matches[0])
|
|
|
verification_link = verification_link.replace('&', '&')
|
|
|
|
|
|
|
|
|
parsed = urlparse(verification_link)
|
|
|
params = parse_qs(parsed.query)
|
|
|
|
|
|
oob_code = params.get('oobCode', [None])[0]
|
|
|
if oob_code:
|
|
|
mail.logout()
|
|
|
logger.info(f"✅ 找到验证码: {oob_code}")
|
|
|
return {
|
|
|
"oob_code": oob_code,
|
|
|
"verification_link": verification_link
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"搜索条件 '{criteria}' 出错: {e}")
|
|
|
continue
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"处理文件夹 {folder} 出错: {e}")
|
|
|
continue
|
|
|
|
|
|
mail.logout()
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"邮件检查失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
def _extract_email_body(self, msg):
|
|
|
"""提取邮件正文内容(处理多部分邮件)"""
|
|
|
body = ""
|
|
|
|
|
|
if msg.is_multipart():
|
|
|
for part in msg.walk():
|
|
|
content_type = part.get_content_type()
|
|
|
content_disposition = str(part.get("Content-Disposition"))
|
|
|
|
|
|
|
|
|
if "attachment" in content_disposition:
|
|
|
continue
|
|
|
|
|
|
if content_type in ["text/plain", "text/html"]:
|
|
|
try:
|
|
|
payload = part.get_payload(decode=True)
|
|
|
if payload:
|
|
|
body += payload.decode('utf-8', errors='ignore')
|
|
|
except Exception as e:
|
|
|
logger.debug(f"解析邮件部分出错: {e}")
|
|
|
else:
|
|
|
|
|
|
try:
|
|
|
payload = msg.get_payload(decode=True)
|
|
|
if payload:
|
|
|
body = payload.decode('utf-8', errors='ignore')
|
|
|
except Exception as e:
|
|
|
logger.debug(f"解析单部分邮件出错: {e}")
|
|
|
|
|
|
return body
|
|
|
|
|
|
def extract_and_recombine_url(self, original_url):
|
|
|
"""
|
|
|
从原始URL中提取参数并重新组合成目标格式
|
|
|
|
|
|
Args:
|
|
|
original_url (str): 原始URL字符串
|
|
|
|
|
|
Returns:
|
|
|
str: 重新组合后的URL
|
|
|
"""
|
|
|
|
|
|
parsed = urlparse(original_url)
|
|
|
|
|
|
|
|
|
query_params = parse_qs(parsed.query)
|
|
|
|
|
|
|
|
|
api_key = query_params.get('apiKey', [''])[0]
|
|
|
mode = query_params.get('mode', [''])[0]
|
|
|
oob_code = query_params.get('oobCode', [''])[0]
|
|
|
lang = query_params.get('lang', [''])[0]
|
|
|
|
|
|
|
|
|
continue_url = query_params.get('continueUrl', [''])[0]
|
|
|
if continue_url:
|
|
|
|
|
|
continue_parsed = urlparse(continue_url)
|
|
|
base_path = continue_parsed.path
|
|
|
else:
|
|
|
base_path = '/login'
|
|
|
|
|
|
|
|
|
new_params = {
|
|
|
'apiKey': api_key,
|
|
|
'oobCode': oob_code,
|
|
|
'mode': mode,
|
|
|
'lang': lang
|
|
|
}
|
|
|
|
|
|
|
|
|
new_url = urlunparse((
|
|
|
'https',
|
|
|
'app.warp.dev',
|
|
|
base_path,
|
|
|
'',
|
|
|
urlencode(new_params),
|
|
|
''
|
|
|
))
|
|
|
|
|
|
return new_url
|
|
|
|
|
|
async def complete_email_signin(self, email: str, oob_code: str) -> Dict[str, Any]:
|
|
|
"""完成邮箱登录"""
|
|
|
api_key = self.get_next_api_key()
|
|
|
url = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink?key={api_key}"
|
|
|
|
|
|
payload = {
|
|
|
"email": email,
|
|
|
"oobCode": oob_code
|
|
|
}
|
|
|
|
|
|
headers = {
|
|
|
"Content-Type": "application/json",
|
|
|
"User-Agent": self.user_agent,
|
|
|
"Accept": "*/*"
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
response = await self.async_client.post(url, json=payload, headers=headers)
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
data = response.json()
|
|
|
logger.info(f"✅ 登录成功 [{email}]: {data}")
|
|
|
return {
|
|
|
"success": True,
|
|
|
"id_token": data.get("idToken"),
|
|
|
"refresh_token": data.get("refreshToken"),
|
|
|
"local_id": data.get("localId"),
|
|
|
"email": data.get("email")
|
|
|
}
|
|
|
else:
|
|
|
logger.error(f"登录失败: {response.status_code}")
|
|
|
return {
|
|
|
"success": False,
|
|
|
"error": response.text
|
|
|
}
|
|
|
except (httpx.ProxyError, ssl.SSLError) as e:
|
|
|
logger.error(f"代理错误: {e}")
|
|
|
return {
|
|
|
"success": False,
|
|
|
"error": "proxy_error"
|
|
|
}
|
|
|
except Exception as e:
|
|
|
logger.error(f"登录异常: {type(e).__name__}: {e}", exc_info=True)
|
|
|
return {
|
|
|
"success": False,
|
|
|
"error": str(e)
|
|
|
}
|
|
|
|
|
|
async def activate_warp_user(self, id_token: str, session_id: str) -> Dict[str, Any]:
|
|
|
"""激活Warp用户"""
|
|
|
url = "https://app.warp.dev/graphql/v2"
|
|
|
|
|
|
query = """mutation GetOrCreateUser($input: GetOrCreateUserInput!, $requestContext: RequestContext!) {\n getOrCreateUser(requestContext: $requestContext, input: $input) {\n __typename\n ... on GetOrCreateUserOutput {\n uid\n isOnboarded\n anonymousUserInfo {\n anonymousUserType\n linkedAt\n __typename\n }\n workspaces {\n joinableTeams {\n teamUid\n numMembers\n name\n teamAcceptingInvites\n __typename\n }\n __typename\n }\n onboardingSurveyStatus\n firstLoginAt\n adminOf\n deletedAnonymousUser\n __typename\n }\n ... on UserFacingError {\n error {\n __typename\n message\n ... on TOSViolationError {\n message\n __typename\n }\n }\n __typename\n }\n }\n}\n"""
|
|
|
|
|
|
data = {
|
|
|
"operationName": "GetOrCreateUser",
|
|
|
"variables": {
|
|
|
"input": {
|
|
|
"sessionId": session_id,
|
|
|
},
|
|
|
"requestContext": {
|
|
|
"clientContext": {},
|
|
|
"osContext": {},
|
|
|
}
|
|
|
},
|
|
|
"query": query
|
|
|
}
|
|
|
|
|
|
headers = {
|
|
|
"Content-Type": "application/json",
|
|
|
"Authorization": f"Bearer {id_token}",
|
|
|
"User-Agent": self.user_agent,
|
|
|
"Accept": "*/*",
|
|
|
"referer": "https://app.warp.dev/login"
|
|
|
}
|
|
|
|
|
|
|
|
|
try:
|
|
|
response = await self.async_client.post(
|
|
|
url,
|
|
|
params={"op": "GetOrCreateUser"},
|
|
|
json=data,
|
|
|
headers=headers
|
|
|
)
|
|
|
print(f"[{response.status_code}] Activate Warp Response: {response.text}")
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
result = response.json()
|
|
|
user_data = result.get("data", {}).get("getOrCreateUser", {})
|
|
|
|
|
|
if user_data.get("__typename") == "GetOrCreateUserOutput":
|
|
|
uid = user_data.get("uid")
|
|
|
logger.info(f"✅ Warp用户激活成功: UID={uid}")
|
|
|
|
|
|
response = await self.async_client.post(
|
|
|
url,
|
|
|
params={"op": "UpdateOnboardingSurveyStatus"},
|
|
|
json={
|
|
|
"operationName": "UpdateOnboardingSurveyStatus",
|
|
|
"variables": {
|
|
|
"input": {"status":"SKIPPED"},
|
|
|
"requestContext": {"osContext":{},"clientContext":{}}
|
|
|
},
|
|
|
"query": "mutation UpdateOnboardingSurveyStatus($input: UpdateOnboardingSurveyStatusInput!, $requestContext: RequestContext!) {\n updateOnboardingSurveyStatus(input: $input, requestContext: $requestContext) {\n __typename\n ... on UpdateOnboardingSurveyStatusOutput {\n status\n responseContext {\n __typename\n }\n __typename\n }\n ... on UserFacingError {\n error {\n message\n __typename\n }\n __typename\n }\n }\n}\n"
|
|
|
},
|
|
|
headers={
|
|
|
"Content-Type": "application/json",
|
|
|
"Authorization": f"Bearer {id_token}",
|
|
|
"User-Agent": self.user_agent,
|
|
|
"Accept": "*/*",
|
|
|
}
|
|
|
)
|
|
|
print(f"Update Onboarding Survey Status Response: {response.text}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"uid": uid
|
|
|
}
|
|
|
|
|
|
return {"success": False, "error": "激活失败"}
|
|
|
|
|
|
except (httpx.ProxyError, ssl.SSLError) as e:
|
|
|
logger.error(f"代理错误: {e}")
|
|
|
return {"success": False, "error": "proxy_error"}
|
|
|
except Exception as e:
|
|
|
logger.error(f"激活Warp用户失败: {type(e).__name__}: {e}", exc_info=True)
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
async def _generate_worker_payload(self, session_id: str) -> Dict[str, Any]:
|
|
|
"""
|
|
|
生成worker请求的payload数据 - 完整的、稳定的版本
|
|
|
所有字段都包含在内,没有任何省略
|
|
|
"""
|
|
|
|
|
|
|
|
|
BASE_PROFILES_EXTENDED = [
|
|
|
{
|
|
|
"profile_name": "Win10_Chrome_NVIDIA_GTX1660Ti",
|
|
|
"os": "Windows",
|
|
|
"os_version": "10.0.0",
|
|
|
"platform": "Win32",
|
|
|
"architecture": "x86",
|
|
|
"bitness": 64,
|
|
|
"vendor": "Google Inc.",
|
|
|
"gpu_vendor_string": "Google Inc. (NVIDIA)",
|
|
|
"gpu_renderer": "ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 Ti (0x00002182) Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
|
"screen_resolution": {"width": 1707, "height": 960},
|
|
|
"hardware_config": {"memory": 8, "cores": 12},
|
|
|
"hashes": {
|
|
|
"prototype_hash": "5051906984991708",
|
|
|
"math_hash": "4407615957639726",
|
|
|
"offline_audio_hash": "733027540168168",
|
|
|
"mime_types_hash": "6633968372405724",
|
|
|
"errors_hash": "1415081268456649"
|
|
|
}
|
|
|
},
|
|
|
{
|
|
|
"profile_name": "Win10_Chrome_AMD_Radeon",
|
|
|
"os": "Windows",
|
|
|
"os_version": "10.0.0",
|
|
|
"platform": "Win32",
|
|
|
"architecture": "x86",
|
|
|
"bitness": 64,
|
|
|
"vendor": "Google Inc.",
|
|
|
"gpu_vendor_string": "Google Inc. (AMD)",
|
|
|
"gpu_renderer": "ANGLE (AMD, AMD Radeon(TM) Graphics (0x00001638) Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
|
"screen_resolution": {"width": 1536, "height": 864},
|
|
|
"hardware_config": {"memory": 8, "cores": 16},
|
|
|
"hashes": {
|
|
|
"prototype_hash": "4842229194603551",
|
|
|
"math_hash": "4407615957639726",
|
|
|
"offline_audio_hash": "733027540168168",
|
|
|
"mime_types_hash": "2795763505992044",
|
|
|
"errors_hash": "1415081268456649"
|
|
|
}
|
|
|
},
|
|
|
{
|
|
|
"profile_name": "Win10_Chrome_NVIDIA_RTX3060",
|
|
|
"os": "Windows",
|
|
|
"os_version": "10.0.0",
|
|
|
"platform": "Win32",
|
|
|
"architecture": "x86",
|
|
|
"bitness": 64,
|
|
|
"vendor": "Google Inc.",
|
|
|
"gpu_vendor_string": "Google Inc. (NVIDIA)",
|
|
|
"gpu_renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 (0x00002503) Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
|
|
"screen_resolution": {"width": 1920, "height": 1080},
|
|
|
"hardware_config": {"memory": 16, "cores": 8},
|
|
|
"hashes": {
|
|
|
"prototype_hash": "5051906984991708",
|
|
|
"math_hash": "4407615957639726",
|
|
|
"offline_audio_hash": "733027540168168",
|
|
|
"mime_types_hash": "6633968372405724",
|
|
|
"errors_hash": "1415081268456649"
|
|
|
}
|
|
|
}
|
|
|
]
|
|
|
|
|
|
|
|
|
BROWSER_VERSIONS_COMPLETE = [
|
|
|
{
|
|
|
"browser": "Chrome",
|
|
|
"major": 140,
|
|
|
"full_version": "140.0.7339.208",
|
|
|
"brands": [
|
|
|
{"brand": "Chromium", "version": "140"},
|
|
|
{"brand": "Not=A?Brand", "version": "24"},
|
|
|
{"brand": "Google Chrome", "version": "140"}
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
"browser": "Edge",
|
|
|
"major": 141,
|
|
|
"full_version": "141.0.3537.57",
|
|
|
"full_chromium_version": "141.0.7390.55",
|
|
|
"brands": [
|
|
|
{"brand": "Microsoft Edge", "version": "141"},
|
|
|
{"brand": "Not?A_Brand", "version": "8"},
|
|
|
{"brand": "Chromium", "version": "141"}
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
"browser": "Chrome",
|
|
|
"major": 128,
|
|
|
"full_version": "128.0.6613.137",
|
|
|
"brands": [
|
|
|
{"brand": "Chromium", "version": "128"},
|
|
|
{"brand": "Not-A.Brand", "version": "24"},
|
|
|
{"brand": "Google Chrome", "version": "128"}
|
|
|
]
|
|
|
},
|
|
|
{
|
|
|
"browser": "Chrome",
|
|
|
"major": 126,
|
|
|
"full_version": "126.0.6478.126",
|
|
|
"brands": [
|
|
|
{"brand": "Chromium", "version": "126"},
|
|
|
{"brand": "Not-A.Brand", "version": "24"},
|
|
|
{"brand": "Google Chrome", "version": "126"}
|
|
|
]
|
|
|
}
|
|
|
]
|
|
|
|
|
|
|
|
|
LANGUAGE_CONFIGS_FIXED = [
|
|
|
{"lang": "zh-CN", "languages": ["zh-CN", "zh"], "timezone_offset": 8, "timezone": "Asia/Shanghai"},
|
|
|
{"lang": "zh-CN", "languages": ["zh-CN", "zh", "en"], "timezone_offset": 8, "timezone": "Asia/Shanghai"},
|
|
|
{"lang": "zh-CN", "languages": ["zh-CN"], "timezone_offset": 8, "timezone": "Asia/Shanghai"},
|
|
|
{"lang": "en-US", "languages": ["en-US", "en"], "timezone_offset": -5, "timezone": "America/New_York"},
|
|
|
{"lang": "en-US", "languages": ["en-US", "en"], "timezone_offset": -7, "timezone": "America/Los_Angeles"}
|
|
|
]
|
|
|
|
|
|
|
|
|
base_window_keys = [
|
|
|
"Object",
|
|
|
"Function",
|
|
|
"Array",
|
|
|
"Number",
|
|
|
"parseFloat",
|
|
|
"parseInt",
|
|
|
"Infinity",
|
|
|
"NaN",
|
|
|
"undefined",
|
|
|
"Boolean",
|
|
|
"String",
|
|
|
"Symbol",
|
|
|
"Date",
|
|
|
"Promise",
|
|
|
"RegExp",
|
|
|
"Error",
|
|
|
"AggregateError",
|
|
|
"EvalError",
|
|
|
"RangeError",
|
|
|
"ReferenceError",
|
|
|
"SyntaxError",
|
|
|
"TypeError",
|
|
|
"URIError",
|
|
|
"globalThis",
|
|
|
"JSON",
|
|
|
"Math",
|
|
|
"Intl",
|
|
|
"ArrayBuffer",
|
|
|
"Atomics",
|
|
|
"Uint8Array",
|
|
|
"Int8Array",
|
|
|
"Uint16Array",
|
|
|
"Int16Array",
|
|
|
"Uint32Array",
|
|
|
"Int32Array",
|
|
|
"BigUint64Array",
|
|
|
"BigInt64Array",
|
|
|
"Uint8ClampedArray",
|
|
|
"Float32Array",
|
|
|
"Float64Array",
|
|
|
"DataView",
|
|
|
"Map",
|
|
|
"BigInt",
|
|
|
"Set",
|
|
|
"Iterator",
|
|
|
"WeakMap",
|
|
|
"WeakSet",
|
|
|
"Proxy",
|
|
|
"Reflect",
|
|
|
"FinalizationRegistry",
|
|
|
"WeakRef",
|
|
|
"decodeURI",
|
|
|
"decodeURIComponent",
|
|
|
"encodeURI",
|
|
|
"encodeURIComponent",
|
|
|
"escape",
|
|
|
"unescape",
|
|
|
"eval",
|
|
|
"isFinite",
|
|
|
"isNaN",
|
|
|
"console",
|
|
|
"Option",
|
|
|
"Image",
|
|
|
"Audio",
|
|
|
"webkitURL",
|
|
|
"webkitRTCPeerConnection",
|
|
|
"webkitMediaStream",
|
|
|
"WebKitMutationObserver",
|
|
|
"WebKitCSSMatrix",
|
|
|
"XSLTProcessor",
|
|
|
"XPathResult",
|
|
|
"XPathExpression",
|
|
|
"XPathEvaluator",
|
|
|
"XMLSerializer",
|
|
|
"XMLHttpRequestUpload",
|
|
|
"XMLHttpRequestEventTarget",
|
|
|
"XMLHttpRequest",
|
|
|
"XMLDocument",
|
|
|
"WritableStreamDefaultWriter",
|
|
|
"WritableStreamDefaultController",
|
|
|
"WritableStream",
|
|
|
"Worker",
|
|
|
"WindowControlsOverlayGeometryChangeEvent",
|
|
|
"WindowControlsOverlay",
|
|
|
"Window",
|
|
|
"WheelEvent",
|
|
|
"WebSocket",
|
|
|
"WebGLVertexArrayObject",
|
|
|
"WebGLUniformLocation",
|
|
|
"WebGLTransformFeedback",
|
|
|
"WebGLTexture",
|
|
|
"WebGLSync",
|
|
|
"WebGLShaderPrecisionFormat",
|
|
|
"WebGLShader",
|
|
|
"WebGLSampler",
|
|
|
"WebGLRenderingContext",
|
|
|
"WebGLRenderbuffer",
|
|
|
"WebGLQuery",
|
|
|
"WebGLProgram",
|
|
|
"WebGLObject",
|
|
|
"WebGLFramebuffer",
|
|
|
"WebGLContextEvent",
|
|
|
"WebGLBuffer",
|
|
|
"WebGLActiveInfo",
|
|
|
"WebGL2RenderingContext",
|
|
|
"WaveShaperNode",
|
|
|
"VisualViewport",
|
|
|
"VisibilityStateEntry",
|
|
|
"VirtualKeyboardGeometryChangeEvent",
|
|
|
"ViewTransitionTypeSet",
|
|
|
"ViewTransition",
|
|
|
"ViewTimeline",
|
|
|
"VideoPlaybackQuality",
|
|
|
"VideoFrame",
|
|
|
"VideoColorSpace",
|
|
|
"ValidityState",
|
|
|
"VTTCue",
|
|
|
"UserActivation",
|
|
|
"URLSearchParams",
|
|
|
"URLPattern",
|
|
|
"URL",
|
|
|
"UIEvent",
|
|
|
"TrustedTypePolicyFactory",
|
|
|
"TrustedTypePolicy",
|
|
|
"TrustedScriptURL",
|
|
|
"TrustedScript",
|
|
|
"TrustedHTML",
|
|
|
"TreeWalker",
|
|
|
"TransitionEvent",
|
|
|
"TransformStreamDefaultController",
|
|
|
"TransformStream",
|
|
|
"TrackEvent",
|
|
|
"TouchList",
|
|
|
"TouchEvent",
|
|
|
"Touch",
|
|
|
"ToggleEvent",
|
|
|
"TimeRanges",
|
|
|
"TextUpdateEvent",
|
|
|
"TextTrackList",
|
|
|
"TextTrackCueList",
|
|
|
"TextTrackCue",
|
|
|
"TextTrack",
|
|
|
"TextMetrics",
|
|
|
"TextFormatUpdateEvent",
|
|
|
"TextFormat",
|
|
|
"TextEvent",
|
|
|
"TextEncoderStream",
|
|
|
"TextEncoder",
|
|
|
"TextDecoderStream",
|
|
|
"TextDecoder",
|
|
|
"Text",
|
|
|
"TaskSignal",
|
|
|
"TaskPriorityChangeEvent",
|
|
|
"TaskController",
|
|
|
"TaskAttributionTiming",
|
|
|
"SyncManager",
|
|
|
"Subscriber",
|
|
|
"SubmitEvent",
|
|
|
"StyleSheetList",
|
|
|
"StyleSheet",
|
|
|
"StylePropertyMapReadOnly",
|
|
|
"StylePropertyMap",
|
|
|
"StorageEvent",
|
|
|
"Storage",
|
|
|
"StereoPannerNode",
|
|
|
"StaticRange",
|
|
|
"SourceBufferList",
|
|
|
"SourceBuffer",
|
|
|
"ShadowRoot",
|
|
|
"Selection",
|
|
|
"SecurityPolicyViolationEvent",
|
|
|
"ScrollTimeline",
|
|
|
"ScriptProcessorNode",
|
|
|
"ScreenOrientation",
|
|
|
"Screen",
|
|
|
"Scheduling",
|
|
|
"Scheduler",
|
|
|
"SVGViewElement",
|
|
|
"SVGUseElement",
|
|
|
"SVGUnitTypes",
|
|
|
"SVGTransformList",
|
|
|
"SVGTransform",
|
|
|
"SVGTitleElement",
|
|
|
"SVGTextPositioningElement",
|
|
|
"SVGTextPathElement",
|
|
|
"SVGTextElement",
|
|
|
"SVGTextContentElement",
|
|
|
"SVGTSpanElement",
|
|
|
"SVGSymbolElement",
|
|
|
"SVGSwitchElement",
|
|
|
"SVGStyleElement",
|
|
|
"SVGStringList",
|
|
|
"SVGStopElement",
|
|
|
"SVGSetElement",
|
|
|
"SVGScriptElement",
|
|
|
"SVGSVGElement",
|
|
|
"SVGRectElement",
|
|
|
"SVGRect",
|
|
|
"SVGRadialGradientElement",
|
|
|
"SVGPreserveAspectRatio",
|
|
|
"SVGPolylineElement",
|
|
|
"SVGPolygonElement",
|
|
|
"SVGPointList",
|
|
|
"SVGPoint",
|
|
|
"SVGPatternElement",
|
|
|
"SVGPathElement",
|
|
|
"SVGNumberList",
|
|
|
"SVGNumber",
|
|
|
"SVGMetadataElement",
|
|
|
"SVGMatrix",
|
|
|
"SVGMaskElement",
|
|
|
"SVGMarkerElement",
|
|
|
"SVGMPathElement",
|
|
|
"SVGLinearGradientElement",
|
|
|
"SVGLineElement",
|
|
|
"SVGLengthList",
|
|
|
"SVGLength",
|
|
|
"SVGImageElement",
|
|
|
"SVGGraphicsElement",
|
|
|
"SVGGradientElement",
|
|
|
"SVGGeometryElement",
|
|
|
"SVGGElement",
|
|
|
"SVGForeignObjectElement",
|
|
|
"SVGFilterElement",
|
|
|
"SVGFETurbulenceElement",
|
|
|
"SVGFETileElement",
|
|
|
"SVGFESpotLightElement",
|
|
|
"SVGFESpecularLightingElement",
|
|
|
"SVGFEPointLightElement",
|
|
|
"SVGFEOffsetElement",
|
|
|
"SVGFEMorphologyElement",
|
|
|
"SVGFEMergeNodeElement",
|
|
|
"SVGFEMergeElement",
|
|
|
"SVGFEImageElement",
|
|
|
"SVGFEGaussianBlurElement",
|
|
|
"SVGFEFuncRElement",
|
|
|
"SVGFEFuncGElement",
|
|
|
"SVGFEFuncBElement",
|
|
|
"SVGFEFuncAElement",
|
|
|
"SVGFEFloodElement",
|
|
|
"SVGFEDropShadowElement",
|
|
|
"SVGFEDistantLightElement",
|
|
|
"SVGFEDisplacementMapElement",
|
|
|
"SVGFEDiffuseLightingElement",
|
|
|
"SVGFEConvolveMatrixElement",
|
|
|
"SVGFECompositeElement",
|
|
|
"SVGFEComponentTransferElement",
|
|
|
"SVGFEColorMatrixElement",
|
|
|
"SVGFEBlendElement",
|
|
|
"SVGEllipseElement",
|
|
|
"SVGElement",
|
|
|
"SVGDescElement",
|
|
|
"SVGDefsElement",
|
|
|
"SVGComponentTransferFunctionElement",
|
|
|
"SVGClipPathElement",
|
|
|
"SVGCircleElement",
|
|
|
"SVGAnimationElement",
|
|
|
"SVGAnimatedTransformList",
|
|
|
"SVGAnimatedString",
|
|
|
"SVGAnimatedRect",
|
|
|
"SVGAnimatedPreserveAspectRatio",
|
|
|
"SVGAnimatedNumberList",
|
|
|
"SVGAnimatedNumber",
|
|
|
"SVGAnimatedLengthList",
|
|
|
"SVGAnimatedLength",
|
|
|
"SVGAnimatedInteger",
|
|
|
"SVGAnimatedEnumeration",
|
|
|
"SVGAnimatedBoolean",
|
|
|
"SVGAnimatedAngle",
|
|
|
"SVGAnimateTransformElement",
|
|
|
"SVGAnimateMotionElement",
|
|
|
"SVGAnimateElement",
|
|
|
"SVGAngle",
|
|
|
"SVGAElement",
|
|
|
"Response",
|
|
|
"ResizeObserverSize",
|
|
|
"ResizeObserverEntry",
|
|
|
"ResizeObserver",
|
|
|
"Request",
|
|
|
"ReportingObserver",
|
|
|
"ReportBody",
|
|
|
"ReadableStreamDefaultReader",
|
|
|
"ReadableStreamDefaultController",
|
|
|
"ReadableStreamBYOBRequest",
|
|
|
"ReadableStreamBYOBReader",
|
|
|
"ReadableStream",
|
|
|
"ReadableByteStreamController",
|
|
|
"Range",
|
|
|
"RadioNodeList",
|
|
|
"RTCTrackEvent",
|
|
|
"RTCStatsReport",
|
|
|
"RTCSessionDescription",
|
|
|
"RTCSctpTransport",
|
|
|
"RTCRtpTransceiver",
|
|
|
"RTCRtpSender",
|
|
|
"RTCRtpReceiver",
|
|
|
"RTCPeerConnectionIceEvent",
|
|
|
"RTCPeerConnectionIceErrorEvent",
|
|
|
"RTCPeerConnection",
|
|
|
"RTCIceTransport",
|
|
|
"RTCIceCandidate",
|
|
|
"RTCErrorEvent",
|
|
|
"RTCError",
|
|
|
"RTCEncodedVideoFrame",
|
|
|
"RTCEncodedAudioFrame",
|
|
|
"RTCDtlsTransport",
|
|
|
"RTCDataChannelEvent",
|
|
|
"RTCDTMFToneChangeEvent",
|
|
|
"RTCDTMFSender",
|
|
|
"RTCCertificate",
|
|
|
"PromiseRejectionEvent",
|
|
|
"ProgressEvent",
|
|
|
"Profiler",
|
|
|
"ProcessingInstruction",
|
|
|
"PopStateEvent",
|
|
|
"PointerEvent",
|
|
|
"PluginArray",
|
|
|
"Plugin",
|
|
|
"PictureInPictureWindow",
|
|
|
"PictureInPictureEvent",
|
|
|
"PeriodicWave",
|
|
|
"PerformanceTiming",
|
|
|
"PerformanceServerTiming",
|
|
|
"PerformanceScriptTiming",
|
|
|
"PerformanceResourceTiming",
|
|
|
"PerformancePaintTiming",
|
|
|
"PerformanceObserverEntryList",
|
|
|
"PerformanceObserver",
|
|
|
"PerformanceNavigationTiming",
|
|
|
"PerformanceNavigation",
|
|
|
"PerformanceMeasure",
|
|
|
"PerformanceMark",
|
|
|
"PerformanceLongTaskTiming",
|
|
|
"PerformanceLongAnimationFrameTiming",
|
|
|
"PerformanceEventTiming",
|
|
|
"PerformanceEntry",
|
|
|
"PerformanceElementTiming",
|
|
|
"Performance",
|
|
|
"Path2D",
|
|
|
"PannerNode",
|
|
|
"PageTransitionEvent",
|
|
|
"OverconstrainedError",
|
|
|
"OscillatorNode",
|
|
|
"OffscreenCanvasRenderingContext2D",
|
|
|
"OffscreenCanvas",
|
|
|
"OfflineAudioContext",
|
|
|
"OfflineAudioCompletionEvent",
|
|
|
"Observable",
|
|
|
"NodeList",
|
|
|
"NodeIterator",
|
|
|
"NodeFilter",
|
|
|
"Node",
|
|
|
"NetworkInformation",
|
|
|
"NavigatorUAData",
|
|
|
"Navigator",
|
|
|
"NavigationTransition",
|
|
|
"NavigationHistoryEntry",
|
|
|
"NavigationDestination",
|
|
|
"NavigationCurrentEntryChangeEvent",
|
|
|
"NavigationActivation",
|
|
|
"Navigation",
|
|
|
"NavigateEvent",
|
|
|
"NamedNodeMap",
|
|
|
"MutationRecord",
|
|
|
"MutationObserver",
|
|
|
"MouseEvent",
|
|
|
"MimeTypeArray",
|
|
|
"MimeType",
|
|
|
"MessagePort",
|
|
|
"MessageEvent",
|
|
|
"MessageChannel",
|
|
|
"MediaStreamTrackVideoStats",
|
|
|
"MediaStreamTrackProcessor",
|
|
|
"MediaStreamTrackGenerator",
|
|
|
"MediaStreamTrackEvent",
|
|
|
"MediaStreamTrackAudioStats",
|
|
|
"MediaStreamTrack",
|
|
|
"MediaStreamEvent",
|
|
|
"MediaStreamAudioSourceNode",
|
|
|
"MediaStreamAudioDestinationNode",
|
|
|
"MediaStream",
|
|
|
"MediaSourceHandle",
|
|
|
"MediaSource",
|
|
|
"MediaRecorder",
|
|
|
"MediaQueryListEvent",
|
|
|
"MediaQueryList",
|
|
|
"MediaList",
|
|
|
"MediaError",
|
|
|
"MediaEncryptedEvent",
|
|
|
"MediaElementAudioSourceNode",
|
|
|
"MediaCapabilities",
|
|
|
"MathMLElement",
|
|
|
"Location",
|
|
|
"LayoutShiftAttribution",
|
|
|
"LayoutShift",
|
|
|
"LargestContentfulPaint",
|
|
|
"KeyframeEffect",
|
|
|
"KeyboardEvent",
|
|
|
"IntersectionObserverEntry",
|
|
|
"IntersectionObserver",
|
|
|
"InputEvent",
|
|
|
"InputDeviceInfo",
|
|
|
"InputDeviceCapabilities",
|
|
|
"Ink",
|
|
|
"ImageData",
|
|
|
"ImageBitmapRenderingContext",
|
|
|
"ImageBitmap",
|
|
|
"IdleDeadline",
|
|
|
"IIRFilterNode",
|
|
|
"IDBVersionChangeEvent",
|
|
|
"IDBTransaction",
|
|
|
"IDBRequest",
|
|
|
"IDBOpenDBRequest",
|
|
|
"IDBObjectStore",
|
|
|
"IDBKeyRange",
|
|
|
"IDBIndex",
|
|
|
"IDBFactory",
|
|
|
"IDBDatabase",
|
|
|
"IDBCursorWithValue",
|
|
|
"IDBCursor",
|
|
|
"History",
|
|
|
"HighlightRegistry",
|
|
|
"Highlight",
|
|
|
"Headers",
|
|
|
"HashChangeEvent",
|
|
|
"HTMLVideoElement",
|
|
|
"HTMLUnknownElement",
|
|
|
"HTMLUListElement",
|
|
|
"HTMLTrackElement",
|
|
|
"HTMLTitleElement",
|
|
|
"HTMLTimeElement",
|
|
|
"HTMLTextAreaElement",
|
|
|
"HTMLTemplateElement",
|
|
|
"HTMLTableSectionElement",
|
|
|
"HTMLTableRowElement",
|
|
|
"HTMLTableElement",
|
|
|
"HTMLTableColElement",
|
|
|
"HTMLTableCellElement",
|
|
|
"HTMLTableCaptionElement",
|
|
|
"HTMLStyleElement",
|
|
|
"HTMLSpanElement",
|
|
|
"HTMLSourceElement",
|
|
|
"HTMLSlotElement",
|
|
|
"HTMLSelectElement",
|
|
|
"HTMLScriptElement",
|
|
|
"HTMLQuoteElement",
|
|
|
"HTMLProgressElement",
|
|
|
"HTMLPreElement",
|
|
|
"HTMLPictureElement",
|
|
|
"HTMLParamElement",
|
|
|
"HTMLParagraphElement",
|
|
|
"HTMLOutputElement",
|
|
|
"HTMLOptionsCollection",
|
|
|
"HTMLOptionElement",
|
|
|
"HTMLOptGroupElement",
|
|
|
"HTMLObjectElement",
|
|
|
"HTMLOListElement",
|
|
|
"HTMLModElement",
|
|
|
"HTMLMeterElement",
|
|
|
"HTMLMetaElement",
|
|
|
"HTMLMenuElement",
|
|
|
"HTMLMediaElement",
|
|
|
"HTMLMarqueeElement",
|
|
|
"HTMLMapElement",
|
|
|
"HTMLLinkElement",
|
|
|
"HTMLLegendElement",
|
|
|
"HTMLLabelElement",
|
|
|
"HTMLLIElement",
|
|
|
"HTMLInputElement",
|
|
|
"HTMLImageElement",
|
|
|
"HTMLIFrameElement",
|
|
|
"HTMLHtmlElement",
|
|
|
"HTMLHeadingElement",
|
|
|
"HTMLHeadElement",
|
|
|
"HTMLHRElement",
|
|
|
"HTMLFrameSetElement",
|
|
|
"HTMLFrameElement",
|
|
|
"HTMLFormElement",
|
|
|
"HTMLFormControlsCollection",
|
|
|
"HTMLFontElement",
|
|
|
"HTMLFieldSetElement",
|
|
|
"HTMLEmbedElement",
|
|
|
"HTMLElement",
|
|
|
"HTMLDocument",
|
|
|
"HTMLDivElement",
|
|
|
"HTMLDirectoryElement",
|
|
|
"HTMLDialogElement",
|
|
|
"HTMLDetailsElement",
|
|
|
"HTMLDataListElement",
|
|
|
"HTMLDataElement",
|
|
|
"HTMLDListElement",
|
|
|
"HTMLCollection",
|
|
|
"HTMLCanvasElement",
|
|
|
"HTMLButtonElement",
|
|
|
"HTMLBodyElement",
|
|
|
"HTMLBaseElement",
|
|
|
"HTMLBRElement",
|
|
|
"HTMLAudioElement",
|
|
|
"HTMLAreaElement",
|
|
|
"HTMLAnchorElement",
|
|
|
"HTMLAllCollection",
|
|
|
"GeolocationPositionError",
|
|
|
"GeolocationPosition",
|
|
|
"GeolocationCoordinates",
|
|
|
"Geolocation",
|
|
|
"GamepadHapticActuator",
|
|
|
"GamepadEvent",
|
|
|
"GamepadButton",
|
|
|
"Gamepad",
|
|
|
"GainNode",
|
|
|
"FormDataEvent",
|
|
|
"FormData",
|
|
|
"FontFaceSetLoadEvent",
|
|
|
"FontFace",
|
|
|
"FocusEvent",
|
|
|
"FileReader",
|
|
|
"FileList",
|
|
|
"File",
|
|
|
"FeaturePolicy",
|
|
|
"External",
|
|
|
"EventTarget",
|
|
|
"EventSource",
|
|
|
"EventCounts",
|
|
|
"Event",
|
|
|
"ErrorEvent",
|
|
|
"EncodedVideoChunk",
|
|
|
"EncodedAudioChunk",
|
|
|
"ElementInternals",
|
|
|
"Element",
|
|
|
"EditContext",
|
|
|
"DynamicsCompressorNode",
|
|
|
"DragEvent",
|
|
|
"DocumentType",
|
|
|
"DocumentTimeline",
|
|
|
"DocumentFragment",
|
|
|
"Document",
|
|
|
"DelegatedInkTrailPresenter",
|
|
|
"DelayNode",
|
|
|
"DecompressionStream",
|
|
|
"DataTransferItemList",
|
|
|
"DataTransferItem",
|
|
|
"DataTransfer",
|
|
|
"DOMTokenList",
|
|
|
"DOMStringMap",
|
|
|
"DOMStringList",
|
|
|
"DOMRectReadOnly",
|
|
|
"DOMRectList",
|
|
|
"DOMRect",
|
|
|
"DOMQuad",
|
|
|
"DOMPointReadOnly",
|
|
|
"DOMPoint",
|
|
|
"DOMParser",
|
|
|
"DOMMatrixReadOnly",
|
|
|
"DOMMatrix",
|
|
|
"DOMImplementation",
|
|
|
"DOMException",
|
|
|
"DOMError",
|
|
|
"CustomStateSet",
|
|
|
"CustomEvent",
|
|
|
"CustomElementRegistry",
|
|
|
"Crypto",
|
|
|
"CountQueuingStrategy",
|
|
|
"ConvolverNode",
|
|
|
"ContentVisibilityAutoStateChangeEvent",
|
|
|
"ConstantSourceNode",
|
|
|
"CompressionStream",
|
|
|
"CompositionEvent",
|
|
|
"Comment",
|
|
|
"CommandEvent",
|
|
|
"CloseWatcher",
|
|
|
"CloseEvent",
|
|
|
"ClipboardEvent",
|
|
|
"CharacterData",
|
|
|
"CharacterBoundsUpdateEvent",
|
|
|
"ChannelSplitterNode",
|
|
|
"ChannelMergerNode",
|
|
|
"CaretPosition",
|
|
|
"CanvasRenderingContext2D",
|
|
|
"CanvasPattern",
|
|
|
"CanvasGradient",
|
|
|
"CanvasCaptureMediaStreamTrack",
|
|
|
"CSSViewTransitionRule",
|
|
|
"CSSVariableReferenceValue",
|
|
|
"CSSUnparsedValue",
|
|
|
"CSSUnitValue",
|
|
|
"CSSTranslate",
|
|
|
"CSSTransition",
|
|
|
"CSSTransformValue",
|
|
|
"CSSTransformComponent",
|
|
|
"CSSSupportsRule",
|
|
|
"CSSStyleValue",
|
|
|
"CSSStyleSheet",
|
|
|
"CSSStyleRule",
|
|
|
"CSSStyleDeclaration",
|
|
|
"CSSStartingStyleRule",
|
|
|
"CSSSkewY",
|
|
|
"CSSSkewX",
|
|
|
"CSSSkew",
|
|
|
"CSSScopeRule",
|
|
|
"CSSScale",
|
|
|
"CSSRuleList",
|
|
|
"CSSRule",
|
|
|
"CSSRotate",
|
|
|
"CSSPropertyRule",
|
|
|
"CSSPositionValue",
|
|
|
"CSSPositionTryRule",
|
|
|
"CSSPositionTryDescriptors",
|
|
|
"CSSPerspective",
|
|
|
"CSSPageRule",
|
|
|
"CSSNumericValue",
|
|
|
"CSSNumericArray",
|
|
|
"CSSNestedDeclarations",
|
|
|
"CSSNamespaceRule",
|
|
|
"CSSMediaRule",
|
|
|
"CSSMatrixComponent",
|
|
|
"CSSMathValue",
|
|
|
"CSSMathSum",
|
|
|
"CSSMathProduct",
|
|
|
"CSSMathNegate",
|
|
|
"CSSMathMin",
|
|
|
"CSSMathMax",
|
|
|
"CSSMathInvert",
|
|
|
"CSSMathClamp",
|
|
|
"CSSMarginRule",
|
|
|
"CSSLayerStatementRule",
|
|
|
"CSSLayerBlockRule",
|
|
|
"CSSKeywordValue",
|
|
|
"CSSKeyframesRule",
|
|
|
"CSSKeyframeRule",
|
|
|
"CSSImportRule",
|
|
|
"CSSImageValue",
|
|
|
"CSSGroupingRule",
|
|
|
"CSSFontPaletteValuesRule",
|
|
|
"CSSFontFaceRule",
|
|
|
"CSSCounterStyleRule",
|
|
|
"CSSContainerRule",
|
|
|
"CSSConditionRule",
|
|
|
"CSSAnimation",
|
|
|
"CSS",
|
|
|
"CSPViolationReportBody",
|
|
|
"CDATASection",
|
|
|
"ByteLengthQueuingStrategy",
|
|
|
"BrowserCaptureMediaStreamTrack",
|
|
|
"BroadcastChannel",
|
|
|
"BlobEvent",
|
|
|
"Blob",
|
|
|
"BiquadFilterNode",
|
|
|
"BeforeUnloadEvent",
|
|
|
"BeforeInstallPromptEvent",
|
|
|
"BaseAudioContext",
|
|
|
"BarProp",
|
|
|
"AudioWorkletNode",
|
|
|
"AudioSinkInfo",
|
|
|
"AudioScheduledSourceNode",
|
|
|
"AudioProcessingEvent",
|
|
|
"AudioParamMap",
|
|
|
"AudioParam",
|
|
|
"AudioNode",
|
|
|
"AudioListener",
|
|
|
"AudioDestinationNode",
|
|
|
"AudioData",
|
|
|
"AudioContext",
|
|
|
"AudioBufferSourceNode",
|
|
|
"AudioBuffer",
|
|
|
"Attr",
|
|
|
"AnimationTimeline",
|
|
|
"AnimationPlaybackEvent",
|
|
|
"AnimationEvent",
|
|
|
"AnimationEffect",
|
|
|
"Animation",
|
|
|
"AnalyserNode",
|
|
|
"AbstractRange",
|
|
|
"AbortSignal",
|
|
|
"AbortController",
|
|
|
"window",
|
|
|
"self",
|
|
|
"document",
|
|
|
"name",
|
|
|
"location",
|
|
|
"customElements",
|
|
|
"history",
|
|
|
"navigation",
|
|
|
"locationbar",
|
|
|
"menubar",
|
|
|
"personalbar",
|
|
|
"scrollbars",
|
|
|
"statusbar",
|
|
|
"toolbar",
|
|
|
"status",
|
|
|
"closed",
|
|
|
"frames",
|
|
|
"length",
|
|
|
"top",
|
|
|
"opener",
|
|
|
"parent",
|
|
|
"frameElement",
|
|
|
"navigator",
|
|
|
"origin",
|
|
|
"external",
|
|
|
"screen",
|
|
|
"innerWidth",
|
|
|
"innerHeight",
|
|
|
"scrollX",
|
|
|
"pageXOffset",
|
|
|
"scrollY",
|
|
|
"pageYOffset",
|
|
|
"visualViewport",
|
|
|
"screenX",
|
|
|
"screenY",
|
|
|
"outerWidth",
|
|
|
"outerHeight",
|
|
|
"devicePixelRatio",
|
|
|
"event",
|
|
|
"clientInformation",
|
|
|
"offscreenBuffering",
|
|
|
"screenLeft",
|
|
|
"screenTop",
|
|
|
"styleMedia",
|
|
|
"onsearch",
|
|
|
"trustedTypes",
|
|
|
"performance",
|
|
|
"onappinstalled",
|
|
|
"onbeforeinstallprompt",
|
|
|
"crypto",
|
|
|
"indexedDB",
|
|
|
"sessionStorage",
|
|
|
"localStorage",
|
|
|
"onbeforexrselect",
|
|
|
"onabort",
|
|
|
"onbeforeinput",
|
|
|
"onbeforematch",
|
|
|
"onbeforetoggle",
|
|
|
"onblur",
|
|
|
"oncancel",
|
|
|
"oncanplay",
|
|
|
"oncanplaythrough",
|
|
|
"onchange",
|
|
|
"onclick",
|
|
|
"onclose",
|
|
|
"oncommand",
|
|
|
"oncontentvisibilityautostatechange",
|
|
|
"oncontextlost",
|
|
|
"oncontextmenu",
|
|
|
"oncontextrestored",
|
|
|
"oncuechange",
|
|
|
"ondblclick",
|
|
|
"ondrag",
|
|
|
"ondragend",
|
|
|
"ondragenter",
|
|
|
"ondragleave",
|
|
|
"ondragover",
|
|
|
"ondragstart",
|
|
|
"ondrop",
|
|
|
"ondurationchange",
|
|
|
"onemptied",
|
|
|
"onended",
|
|
|
"onerror",
|
|
|
"onfocus",
|
|
|
"onformdata",
|
|
|
"oninput",
|
|
|
"oninvalid",
|
|
|
"onkeydown",
|
|
|
"onkeypress",
|
|
|
"onkeyup",
|
|
|
"onload",
|
|
|
"onloadeddata",
|
|
|
"onloadedmetadata",
|
|
|
"onloadstart",
|
|
|
"onmousedown",
|
|
|
"onmouseenter",
|
|
|
"onmouseleave",
|
|
|
"onmousemove",
|
|
|
"onmouseout",
|
|
|
"onmouseover",
|
|
|
"onmouseup",
|
|
|
"onmousewheel",
|
|
|
"onpause",
|
|
|
"onplay",
|
|
|
"onplaying",
|
|
|
"onprogress",
|
|
|
"onratechange",
|
|
|
"onreset",
|
|
|
"onresize",
|
|
|
"onscroll",
|
|
|
"onscrollend",
|
|
|
"onsecuritypolicyviolation",
|
|
|
"onseeked",
|
|
|
"onseeking",
|
|
|
"onselect",
|
|
|
"onslotchange",
|
|
|
"onstalled",
|
|
|
"onsubmit",
|
|
|
"onsuspend",
|
|
|
"ontimeupdate",
|
|
|
"ontoggle",
|
|
|
"onvolumechange",
|
|
|
"onwaiting",
|
|
|
"onwebkitanimationend",
|
|
|
"onwebkitanimationiteration",
|
|
|
"onwebkitanimationstart",
|
|
|
"onwebkittransitionend",
|
|
|
"onwheel",
|
|
|
"onauxclick",
|
|
|
"ongotpointercapture",
|
|
|
"onlostpointercapture",
|
|
|
"onpointerdown",
|
|
|
"onpointermove",
|
|
|
"onpointerrawupdate",
|
|
|
"onpointerup",
|
|
|
"onpointercancel",
|
|
|
"onpointerover",
|
|
|
"onpointerout",
|
|
|
"onpointerenter",
|
|
|
"onpointerleave",
|
|
|
"onselectstart",
|
|
|
"onselectionchange",
|
|
|
"onanimationend",
|
|
|
"onanimationiteration",
|
|
|
"onanimationstart",
|
|
|
"ontransitionrun",
|
|
|
"ontransitionstart",
|
|
|
"ontransitionend",
|
|
|
"ontransitioncancel",
|
|
|
"onafterprint",
|
|
|
"onbeforeprint",
|
|
|
"onbeforeunload",
|
|
|
"onhashchange",
|
|
|
"onlanguagechange",
|
|
|
"onmessage",
|
|
|
"onmessageerror",
|
|
|
"onoffline",
|
|
|
"ononline",
|
|
|
"onpagehide",
|
|
|
"onpageshow",
|
|
|
"onpopstate",
|
|
|
"onrejectionhandled",
|
|
|
"onstorage",
|
|
|
"onunhandledrejection",
|
|
|
"onunload",
|
|
|
"isSecureContext",
|
|
|
"crossOriginIsolated",
|
|
|
"scheduler",
|
|
|
"alert",
|
|
|
"atob",
|
|
|
"blur",
|
|
|
"btoa",
|
|
|
"cancelAnimationFrame",
|
|
|
"cancelIdleCallback",
|
|
|
"captureEvents",
|
|
|
"clearInterval",
|
|
|
"clearTimeout",
|
|
|
"close",
|
|
|
"confirm",
|
|
|
"createImageBitmap",
|
|
|
"fetch",
|
|
|
"find",
|
|
|
"focus",
|
|
|
"getComputedStyle",
|
|
|
"getSelection",
|
|
|
"matchMedia",
|
|
|
"moveBy",
|
|
|
"moveTo",
|
|
|
"open",
|
|
|
"postMessage",
|
|
|
"print",
|
|
|
"prompt",
|
|
|
"queueMicrotask",
|
|
|
"releaseEvents",
|
|
|
"reportError",
|
|
|
"requestAnimationFrame",
|
|
|
"requestIdleCallback",
|
|
|
"resizeBy",
|
|
|
"resizeTo",
|
|
|
"scroll",
|
|
|
"scrollBy",
|
|
|
"scrollTo",
|
|
|
"setInterval",
|
|
|
"setTimeout",
|
|
|
"stop",
|
|
|
"structuredClone",
|
|
|
"webkitCancelAnimationFrame",
|
|
|
"webkitRequestAnimationFrame",
|
|
|
"SuppressedError",
|
|
|
"DisposableStack",
|
|
|
"AsyncDisposableStack",
|
|
|
"Float16Array",
|
|
|
"chrome",
|
|
|
"WebAssembly",
|
|
|
"caches",
|
|
|
"cookieStore",
|
|
|
"ondevicemotion",
|
|
|
"ondeviceorientation",
|
|
|
"ondeviceorientationabsolute",
|
|
|
"documentPictureInPicture",
|
|
|
"sharedStorage",
|
|
|
"AbsoluteOrientationSensor",
|
|
|
"Accelerometer",
|
|
|
"AudioDecoder",
|
|
|
"AudioEncoder",
|
|
|
"AudioWorklet",
|
|
|
"BatteryManager",
|
|
|
"Cache",
|
|
|
"CacheStorage",
|
|
|
"Clipboard",
|
|
|
"ClipboardItem",
|
|
|
"CookieChangeEvent",
|
|
|
"CookieStore",
|
|
|
"CookieStoreManager",
|
|
|
"Credential",
|
|
|
"CredentialsContainer",
|
|
|
"CryptoKey",
|
|
|
"DeviceMotionEvent",
|
|
|
"DeviceMotionEventAcceleration",
|
|
|
"DeviceMotionEventRotationRate",
|
|
|
"DeviceOrientationEvent",
|
|
|
"FederatedCredential",
|
|
|
"GPU",
|
|
|
"GPUAdapter",
|
|
|
"GPUAdapterInfo",
|
|
|
"GPUBindGroup",
|
|
|
"GPUBindGroupLayout",
|
|
|
"GPUBuffer",
|
|
|
"GPUBufferUsage",
|
|
|
"GPUCanvasContext",
|
|
|
"GPUColorWrite",
|
|
|
"GPUCommandBuffer",
|
|
|
"GPUCommandEncoder",
|
|
|
"GPUCompilationInfo",
|
|
|
"GPUCompilationMessage",
|
|
|
"GPUComputePassEncoder",
|
|
|
"GPUComputePipeline",
|
|
|
"GPUDevice",
|
|
|
"GPUDeviceLostInfo",
|
|
|
"GPUError",
|
|
|
"GPUExternalTexture",
|
|
|
"GPUInternalError",
|
|
|
"GPUMapMode",
|
|
|
"GPUOutOfMemoryError",
|
|
|
"GPUPipelineError",
|
|
|
"GPUPipelineLayout",
|
|
|
"GPUQuerySet",
|
|
|
"GPUQueue",
|
|
|
"GPURenderBundle",
|
|
|
"GPURenderBundleEncoder",
|
|
|
"GPURenderPassEncoder",
|
|
|
"GPURenderPipeline",
|
|
|
"GPUSampler",
|
|
|
"GPUShaderModule",
|
|
|
"GPUShaderStage",
|
|
|
"GPUSupportedFeatures",
|
|
|
"GPUSupportedLimits",
|
|
|
"GPUTexture",
|
|
|
"GPUTextureUsage",
|
|
|
"GPUTextureView",
|
|
|
"GPUUncapturedErrorEvent",
|
|
|
"GPUValidationError",
|
|
|
"GravitySensor",
|
|
|
"Gyroscope",
|
|
|
"IdleDetector",
|
|
|
"ImageCapture",
|
|
|
"ImageDecoder",
|
|
|
"ImageTrack",
|
|
|
"ImageTrackList",
|
|
|
"Keyboard",
|
|
|
"KeyboardLayoutMap",
|
|
|
"LinearAccelerationSensor",
|
|
|
"MIDIAccess",
|
|
|
"MIDIConnectionEvent",
|
|
|
"MIDIInput",
|
|
|
"MIDIInputMap",
|
|
|
"MIDIMessageEvent",
|
|
|
"MIDIOutput",
|
|
|
"MIDIOutputMap",
|
|
|
"MIDIPort",
|
|
|
"MediaDeviceInfo",
|
|
|
"MediaDevices",
|
|
|
"MediaKeyMessageEvent",
|
|
|
"MediaKeySession",
|
|
|
"MediaKeyStatusMap",
|
|
|
"MediaKeySystemAccess",
|
|
|
"MediaKeys",
|
|
|
"NavigationPreloadManager",
|
|
|
"NavigatorManagedData",
|
|
|
"OrientationSensor",
|
|
|
"PasswordCredential",
|
|
|
"ProtectedAudience",
|
|
|
"RelativeOrientationSensor",
|
|
|
"ScreenDetailed",
|
|
|
"ScreenDetails",
|
|
|
"Sensor",
|
|
|
"SensorErrorEvent",
|
|
|
"ServiceWorkerRegistration",
|
|
|
"StorageManager",
|
|
|
"SubtleCrypto",
|
|
|
"VideoDecoder",
|
|
|
"VideoEncoder",
|
|
|
"VirtualKeyboard",
|
|
|
"WGSLLanguageFeatures",
|
|
|
"WebTransport",
|
|
|
"WebTransportBidirectionalStream",
|
|
|
"WebTransportDatagramDuplexStream",
|
|
|
"WebTransportError",
|
|
|
"Worklet",
|
|
|
"XRDOMOverlayState",
|
|
|
"XRLayer",
|
|
|
"XRWebGLBinding",
|
|
|
"AuthenticatorAssertionResponse",
|
|
|
"AuthenticatorAttestationResponse",
|
|
|
"AuthenticatorResponse",
|
|
|
"PublicKeyCredential",
|
|
|
"Bluetooth",
|
|
|
"BluetoothCharacteristicProperties",
|
|
|
"BluetoothDevice",
|
|
|
"BluetoothRemoteGATTCharacteristic",
|
|
|
"BluetoothRemoteGATTDescriptor",
|
|
|
"BluetoothRemoteGATTServer",
|
|
|
"BluetoothRemoteGATTService",
|
|
|
"CaptureController",
|
|
|
"CreateMonitor",
|
|
|
"DevicePosture",
|
|
|
"DocumentPictureInPicture",
|
|
|
"EyeDropper",
|
|
|
"FetchLaterResult",
|
|
|
"FileSystemDirectoryHandle",
|
|
|
"FileSystemFileHandle",
|
|
|
"FileSystemHandle",
|
|
|
"FileSystemWritableFileStream",
|
|
|
"FileSystemObserver",
|
|
|
"FontData",
|
|
|
"FragmentDirective",
|
|
|
"HID",
|
|
|
"HIDConnectionEvent",
|
|
|
"HIDDevice",
|
|
|
"HIDInputReportEvent",
|
|
|
"IdentityCredential",
|
|
|
"IdentityCredentialError",
|
|
|
"IdentityProvider",
|
|
|
"NavigatorLogin",
|
|
|
"LanguageDetector",
|
|
|
"Lock",
|
|
|
"LockManager",
|
|
|
"ServiceWorker",
|
|
|
"ServiceWorkerContainer",
|
|
|
"NotRestoredReasonDetails",
|
|
|
"NotRestoredReasons",
|
|
|
"OTPCredential",
|
|
|
"PaymentAddress",
|
|
|
"PaymentRequest",
|
|
|
"PaymentRequestUpdateEvent",
|
|
|
"PaymentResponse",
|
|
|
"PaymentManager",
|
|
|
"PaymentMethodChangeEvent",
|
|
|
"Presentation",
|
|
|
"PresentationAvailability",
|
|
|
"PresentationConnection",
|
|
|
"PresentationConnectionAvailableEvent",
|
|
|
"PresentationConnectionCloseEvent",
|
|
|
"PresentationConnectionList",
|
|
|
"PresentationReceiver",
|
|
|
"PresentationRequest",
|
|
|
"PressureObserver",
|
|
|
"PressureRecord",
|
|
|
"Serial",
|
|
|
"SerialPort",
|
|
|
"SharedWorker",
|
|
|
"StorageBucket",
|
|
|
"StorageBucketManager",
|
|
|
"Summarizer",
|
|
|
"Translator",
|
|
|
"USB",
|
|
|
"USBAlternateInterface",
|
|
|
"USBConfiguration",
|
|
|
"USBConnectionEvent",
|
|
|
"USBDevice",
|
|
|
"USBEndpoint",
|
|
|
"USBInTransferResult",
|
|
|
"USBInterface",
|
|
|
"USBIsochronousInTransferPacket",
|
|
|
"USBIsochronousInTransferResult",
|
|
|
"USBIsochronousOutTransferPacket",
|
|
|
"USBIsochronousOutTransferResult",
|
|
|
"USBOutTransferResult",
|
|
|
"WakeLock",
|
|
|
"WakeLockSentinel",
|
|
|
"XRAnchor",
|
|
|
"XRAnchorSet",
|
|
|
"XRBoundedReferenceSpace",
|
|
|
"XRCPUDepthInformation",
|
|
|
"XRCamera",
|
|
|
"XRDepthInformation",
|
|
|
"XRFrame",
|
|
|
"XRHand",
|
|
|
"XRHitTestResult",
|
|
|
"XRHitTestSource",
|
|
|
"XRInputSource",
|
|
|
"XRInputSourceArray",
|
|
|
"XRInputSourceEvent",
|
|
|
"XRInputSourcesChangeEvent",
|
|
|
"XRJointPose",
|
|
|
"XRJointSpace",
|
|
|
"XRLightEstimate",
|
|
|
"XRLightProbe",
|
|
|
"XRPose",
|
|
|
"XRRay",
|
|
|
"XRReferenceSpace",
|
|
|
"XRReferenceSpaceEvent",
|
|
|
"XRRenderState",
|
|
|
"XRRigidTransform",
|
|
|
"XRSession",
|
|
|
"XRSessionEvent",
|
|
|
"XRSpace",
|
|
|
"XRSystem",
|
|
|
"XRTransientInputHitTestResult",
|
|
|
"XRTransientInputHitTestSource",
|
|
|
"XRView",
|
|
|
"XRViewerPose",
|
|
|
"XRViewport",
|
|
|
"XRWebGLDepthInformation",
|
|
|
"XRWebGLLayer",
|
|
|
"fetchLater",
|
|
|
"getScreenDetails",
|
|
|
"queryLocalFonts",
|
|
|
"showDirectoryPicker",
|
|
|
"showOpenFilePicker",
|
|
|
"showSaveFilePicker",
|
|
|
"originAgentCluster",
|
|
|
"viewport",
|
|
|
"onpageswap",
|
|
|
"onpagereveal",
|
|
|
"credentialless",
|
|
|
"fence",
|
|
|
"launchQueue",
|
|
|
"speechSynthesis",
|
|
|
"onscrollsnapchange",
|
|
|
"onscrollsnapchanging",
|
|
|
"BackgroundFetchManager",
|
|
|
"BackgroundFetchRecord",
|
|
|
"BackgroundFetchRegistration",
|
|
|
"BluetoothUUID",
|
|
|
"CSSFontFeatureValuesRule",
|
|
|
"CSSFunctionDeclarations",
|
|
|
"CSSFunctionDescriptors",
|
|
|
"CSSFunctionRule",
|
|
|
"ChapterInformation",
|
|
|
"CropTarget",
|
|
|
"DocumentPictureInPictureEvent",
|
|
|
"Fence",
|
|
|
"FencedFrameConfig",
|
|
|
"HTMLFencedFrameElement",
|
|
|
"HTMLSelectedContentElement",
|
|
|
"IntegrityViolationReportBody",
|
|
|
"LaunchParams",
|
|
|
"LaunchQueue",
|
|
|
"MediaMetadata",
|
|
|
"MediaSession",
|
|
|
"Notification",
|
|
|
"PageRevealEvent",
|
|
|
"PageSwapEvent",
|
|
|
"PeriodicSyncManager",
|
|
|
"PermissionStatus",
|
|
|
"Permissions",
|
|
|
"PushManager",
|
|
|
"PushSubscription",
|
|
|
"PushSubscriptionOptions",
|
|
|
"QuotaExceededError",
|
|
|
"RTCDataChannel",
|
|
|
"RemotePlayback",
|
|
|
"RestrictionTarget",
|
|
|
"SharedStorage",
|
|
|
"SharedStorageWorklet",
|
|
|
"SharedStorageAppendMethod",
|
|
|
"SharedStorageClearMethod",
|
|
|
"SharedStorageDeleteMethod",
|
|
|
"SharedStorageModifierMethod",
|
|
|
"SharedStorageSetMethod",
|
|
|
"SnapEvent",
|
|
|
"SpeechGrammar",
|
|
|
"SpeechGrammarList",
|
|
|
"SpeechRecognition",
|
|
|
"SpeechRecognitionErrorEvent",
|
|
|
"SpeechRecognitionEvent",
|
|
|
"SpeechSynthesis",
|
|
|
"SpeechSynthesisErrorEvent",
|
|
|
"SpeechSynthesisEvent",
|
|
|
"SpeechSynthesisUtterance",
|
|
|
"SpeechSynthesisVoice",
|
|
|
"Viewport",
|
|
|
"WebSocketError",
|
|
|
"WebSocketStream",
|
|
|
"webkitSpeechGrammar",
|
|
|
"webkitSpeechGrammarList",
|
|
|
"webkitSpeechRecognition",
|
|
|
"webkitSpeechRecognitionError",
|
|
|
"webkitSpeechRecognitionEvent",
|
|
|
"webkitRequestFileSystem",
|
|
|
"webkitResolveLocalFileSystemURL",
|
|
|
"RudderSnippetVersion",
|
|
|
"rudderanalytics",
|
|
|
"rudderAnalyticsBuildType",
|
|
|
"rudderAnalyticsAddScript",
|
|
|
"rudderAnalyticsMount",
|
|
|
"goVerisoulEnv",
|
|
|
"goVerisoulProjectId",
|
|
|
"openBraces",
|
|
|
"script",
|
|
|
"warp_app_base_url",
|
|
|
"warp_app_version",
|
|
|
"verisoul_env",
|
|
|
"verisoul_project_id",
|
|
|
"Verisoul",
|
|
|
"_hsq",
|
|
|
"_hsp",
|
|
|
"RudderStackGlobals",
|
|
|
"__reactRouterVersion",
|
|
|
"warpEmitEvent",
|
|
|
"@wry/context:Slot",
|
|
|
"warpUserHandoff",
|
|
|
"__APOLLO_CLIENT__",
|
|
|
"dataLayer",
|
|
|
"gtag",
|
|
|
"__SENTRY__",
|
|
|
"_0x28b5",
|
|
|
"_0x70cb",
|
|
|
"VerisoulBundleInternal",
|
|
|
"detectIncognito"
|
|
|
]
|
|
|
|
|
|
|
|
|
SITE_SPECIFIC_WINDOW_KEYS = [
|
|
|
"RudderSnippetVersion", "rudderanalytics", "rudderAnalyticsBuildType",
|
|
|
"rudderAnalyticsAddScript", "rudderAnalyticsMount", "goVerisoulEnv",
|
|
|
"goVerisoulProjectId", "openBraces", "script", "warp_app_base_url",
|
|
|
"warp_app_version", "verisoul_env", "verisoul_project_id", "Verisoul",
|
|
|
"_hsq", "_hsp", "RudderStackGlobals", "__reactRouterVersion",
|
|
|
"warpEmitEvent", "@wry/context:Slot", "warpUserHandoff",
|
|
|
"__APOLLO_CLIENT__", "dataLayer", "gtag", "__SENTRY__",
|
|
|
"_0x28b5", "_0x70cb", "VerisoulBundleInternal", "detectIncognito"
|
|
|
]
|
|
|
|
|
|
|
|
|
profile = copy.deepcopy(random.choice(BASE_PROFILES_EXTENDED))
|
|
|
browser_version = random.choice(BROWSER_VERSIONS_COMPLETE)
|
|
|
lang_config = random.choice(LANGUAGE_CONFIGS_FIXED)
|
|
|
|
|
|
|
|
|
ua_full_version = browser_version["full_version"]
|
|
|
if browser_version["browser"] == "Chrome":
|
|
|
user_agent = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{ua_full_version} Safari/537.36"
|
|
|
else:
|
|
|
chromium_version = browser_version.get("full_chromium_version", ua_full_version)
|
|
|
user_agent = f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{chromium_version} Safari/537.36 Edg/{ua_full_version}"
|
|
|
|
|
|
app_version = user_agent.replace("Mozilla/", "")
|
|
|
|
|
|
|
|
|
brands = browser_version["brands"]
|
|
|
full_version_list = []
|
|
|
for brand_info in brands:
|
|
|
if "Chrome" in brand_info["brand"] or "Chromium" in brand_info["brand"] or "Edge" in brand_info["brand"]:
|
|
|
version_str = ua_full_version if "Edge" not in brand_info["brand"] else browser_version.get(
|
|
|
"full_version")
|
|
|
if "Chromium" in brand_info["brand"] and browser_version["browser"] == "Edge":
|
|
|
version_str = browser_version.get("full_chromium_version", ua_full_version)
|
|
|
else:
|
|
|
version_str = f"{brand_info['version']}.0.0.0"
|
|
|
|
|
|
full_version_list.append({
|
|
|
"brand": brand_info["brand"],
|
|
|
"version": version_str
|
|
|
})
|
|
|
|
|
|
|
|
|
hw_config = profile["hardware_config"]
|
|
|
screen_config = profile["screen_resolution"]
|
|
|
|
|
|
|
|
|
keyboard_layout_base = [
|
|
|
{"key": "KeyA", "value": "a"}, {"key": "KeyB", "value": "b"}, {"key": "KeyC", "value": "c"},
|
|
|
{"key": "KeyD", "value": "d"}, {"key": "KeyE", "value": "e"}, {"key": "KeyF", "value": "f"},
|
|
|
{"key": "KeyG", "value": "g"}, {"key": "KeyH", "value": "h"}, {"key": "KeyI", "value": "i"},
|
|
|
{"key": "KeyJ", "value": "j"}, {"key": "KeyK", "value": "k"}, {"key": "KeyL", "value": "l"},
|
|
|
{"key": "KeyM", "value": "m"}, {"key": "KeyN", "value": "n"}, {"key": "KeyO", "value": "o"},
|
|
|
{"key": "KeyP", "value": "p"}, {"key": "KeyQ", "value": "q"}, {"key": "KeyR", "value": "r"},
|
|
|
{"key": "KeyS", "value": "s"}, {"key": "KeyT", "value": "t"}, {"key": "KeyU", "value": "u"},
|
|
|
{"key": "KeyV", "value": "v"}, {"key": "KeyW", "value": "w"}, {"key": "KeyX", "value": "x"},
|
|
|
{"key": "KeyY", "value": "y"}, {"key": "KeyZ", "value": "z"}, {"key": "Digit0", "value": "0"},
|
|
|
{"key": "Digit1", "value": "1"}, {"key": "Digit2", "value": "2"}, {"key": "Digit3", "value": "3"},
|
|
|
{"key": "Digit4", "value": "4"}, {"key": "Digit5", "value": "5"}, {"key": "Digit6", "value": "6"},
|
|
|
{"key": "Digit7", "value": "7"}, {"key": "Digit8", "value": "8"}, {"key": "Digit9", "value": "9"},
|
|
|
{"key": "Backquote", "value": "`"}, {"key": "Minus", "value": "-"}, {"key": "Equal", "value": "="},
|
|
|
{"key": "Backslash", "value": "\\"}, {"key": "BracketLeft", "value": "["},
|
|
|
{"key": "BracketRight", "value": "]"}, {"key": "Semicolon", "value": ";"},
|
|
|
{"key": "Quote", "value": "'"}, {"key": "Comma", "value": ","}, {"key": "Period", "value": "."},
|
|
|
{"key": "Slash", "value": "/"}, {"key": "IntlBackslash", "value": "\\"}
|
|
|
]
|
|
|
random.shuffle(keyboard_layout_base)
|
|
|
|
|
|
|
|
|
now = int(datetime.now().timestamp() * 1000)
|
|
|
navigation_start = now - random.randint(3000, 6000)
|
|
|
fetch_start = navigation_start + random.randint(2, 10)
|
|
|
domain_lookup_start = fetch_start + random.randint(1, 20)
|
|
|
domain_lookup_end = domain_lookup_start + random.randint(10, 50)
|
|
|
connect_start = domain_lookup_end
|
|
|
secure_connection_start = connect_start + random.randint(5, 15) if random.random() > 0.3 else 0
|
|
|
connect_end = (secure_connection_start if secure_connection_start else connect_start) + random.randint(20, 60)
|
|
|
request_start = connect_end + random.randint(1, 5)
|
|
|
response_start = request_start + random.randint(80, 200)
|
|
|
response_end = response_start + random.randint(1, 10)
|
|
|
dom_loading = response_end + random.randint(1, 10)
|
|
|
unload_event_start = dom_loading + random.randint(1, 5)
|
|
|
unload_event_end = unload_event_start + random.randint(1, 3)
|
|
|
dom_interactive = dom_loading + random.randint(200, 500)
|
|
|
dom_content_loaded_event_start = dom_interactive + random.randint(100, 300)
|
|
|
dom_content_loaded_event_end = dom_content_loaded_event_start + random.randint(1, 5)
|
|
|
|
|
|
|
|
|
if random.random() > 0.5:
|
|
|
dom_complete = dom_content_loaded_event_end + random.randint(50, 200)
|
|
|
load_event_start = dom_complete + random.randint(1, 5)
|
|
|
load_event_end = load_event_start + random.randint(1, 5)
|
|
|
else:
|
|
|
dom_complete = 0
|
|
|
load_event_start = 0
|
|
|
load_event_end = 0
|
|
|
|
|
|
performance_timing = {
|
|
|
"navigation_start": navigation_start,
|
|
|
"redirect_start": 0,
|
|
|
"redirect_end": 0,
|
|
|
"fetch_start": fetch_start,
|
|
|
"domain_lookup_start": domain_lookup_start,
|
|
|
"domain_lookup_end": domain_lookup_end,
|
|
|
"connect_start": connect_start,
|
|
|
"secure_connection_start": secure_connection_start,
|
|
|
"connect_end": connect_end,
|
|
|
"request_start": request_start,
|
|
|
"response_start": response_start,
|
|
|
"response_end": response_end,
|
|
|
"unload_event_start": unload_event_start,
|
|
|
"unload_event_end": unload_event_end,
|
|
|
"dom_loading": dom_loading,
|
|
|
"dom_interactive": dom_interactive,
|
|
|
"dom_content_loaded_event_start": dom_content_loaded_event_start,
|
|
|
"dom_content_loaded_event_end": dom_content_loaded_event_end,
|
|
|
"dom_complete": dom_complete,
|
|
|
"load_event_start": load_event_start,
|
|
|
"load_event_end": load_event_end
|
|
|
}
|
|
|
|
|
|
|
|
|
permissions = {
|
|
|
"accessibility_events": "Failed to execute 'query' on 'Permissions': Failed to read the 'name' property from 'PermissionDescriptor': The provided value 'accessibility-events' is not a valid enum value of type PermissionName.",
|
|
|
"ambient_light_sensor": "Failed to execute 'query' on 'Permissions': GenericSensorExtraClasses flag is not enabled.",
|
|
|
"bluetooth": "Failed to execute 'query' on 'Permissions': Failed to read the 'name' property from 'PermissionDescriptor': The provided value 'bluetooth' is not a valid enum value of type PermissionName.",
|
|
|
"nfc": "Failed to execute 'query' on 'Permissions': Web NFC is not enabled.",
|
|
|
"push": "Failed to execute 'query' on 'Permissions': Push Permission without userVisibleOnly:true isn't supported yet.",
|
|
|
"speaker": "Failed to execute 'query' on 'Permissions': Failed to read the 'name' property from 'PermissionDescriptor': The provided value 'speaker' is not a valid enum value of type PermissionName.",
|
|
|
"speaker_selection": "Failed to execute 'query' on 'Permissions': The Speaker Selection API is not enabled.",
|
|
|
"top_level_storage_access": "Failed to execute 'query' on 'Permissions': The requested origin is invalid.",
|
|
|
"accelerometer": "granted",
|
|
|
"background_sync": "granted",
|
|
|
"background_fetch": "granted",
|
|
|
"camera": random.choice(["denied", "prompt"]),
|
|
|
"clipboard_read": random.choice(["prompt", "granted"]),
|
|
|
"clipboard_write": "granted",
|
|
|
"geolocation": random.choice(["denied", "prompt"]),
|
|
|
"gyroscope": "granted",
|
|
|
"local_fonts": "prompt",
|
|
|
"magnetometer": "granted",
|
|
|
"microphone": random.choice(["denied", "prompt"]),
|
|
|
"midi": "prompt",
|
|
|
"notifications": random.choice(["denied", "prompt", "granted"]),
|
|
|
"payment_handler": "granted",
|
|
|
"persistent_storage": "prompt",
|
|
|
"screen_wake_lock": "granted",
|
|
|
"storage_access": "granted",
|
|
|
"window_management": "prompt"
|
|
|
}
|
|
|
|
|
|
|
|
|
window_keys = base_window_keys.copy()
|
|
|
for site_key in SITE_SPECIFIC_WINDOW_KEYS:
|
|
|
if site_key not in window_keys:
|
|
|
window_keys.append(site_key)
|
|
|
|
|
|
|
|
|
incognito = random.choice([0, 1])
|
|
|
if incognito == 1:
|
|
|
storage_quota = random.randint(1000000000, 2000000000)
|
|
|
storage_usage = 0
|
|
|
else:
|
|
|
storage_quota = random.randint(100000000000, 200000000000)
|
|
|
storage_usage = random.randint(1000, 10000000)
|
|
|
|
|
|
|
|
|
connection_rtt = random.choice([50, 100, 150, 200, 250, 300])
|
|
|
connection_downlink = random.choice([0.35, 0.7, 1.25, 1.3, 1.5, 2.5, 5.0, 10.0])
|
|
|
connection_effective_type = random.choice(["slow-2g", "2g", "3g", "4g"])
|
|
|
|
|
|
|
|
|
battery_charging = random.choice([0, 1])
|
|
|
battery_level = round(random.uniform(0.3, 1.0), 2)
|
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"event_id": str(uuid.uuid4()),
|
|
|
"session_id": session_id,
|
|
|
"browser_id": str(uuid.uuid4()),
|
|
|
"project_id": "27fcb93a-7693-486d-b969-a9d96f799f91",
|
|
|
"time": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
|
|
"is_v2": True,
|
|
|
"event": "device_minmet",
|
|
|
|
|
|
|
|
|
"window_keys": window_keys,
|
|
|
"document_element_keys": ["lang"],
|
|
|
|
|
|
|
|
|
"engine": "WebKit",
|
|
|
|
|
|
|
|
|
"max_touch_points": 0 if profile["os"] == "Windows" else 1,
|
|
|
|
|
|
|
|
|
"language": lang_config["lang"],
|
|
|
"languages": lang_config["languages"],
|
|
|
|
|
|
|
|
|
"document_cookies": "",
|
|
|
|
|
|
|
|
|
"performance_timing": performance_timing,
|
|
|
|
|
|
|
|
|
"app_code_name": "Mozilla",
|
|
|
"app_name": "Netscape",
|
|
|
"app_version": app_version,
|
|
|
"device_memory": hw_config["memory"],
|
|
|
"hardware_concurrency": hw_config["cores"],
|
|
|
"platform": profile["platform"],
|
|
|
"product": "Gecko",
|
|
|
"product_sub": "20030107",
|
|
|
"user_agent": user_agent,
|
|
|
"vendor": profile["vendor"],
|
|
|
"vendor_sub": "empty_string",
|
|
|
"cookie_enabled": 1,
|
|
|
"do_not_track": -888,
|
|
|
"webdriver": 0,
|
|
|
"on_line": 1,
|
|
|
|
|
|
|
|
|
"js_heap_size_limit": random.randint(2147483648, 4294967296),
|
|
|
"used_js_heap_size": random.randint(5000000, 50000000),
|
|
|
"total_js_heap_size": random.randint(7000000, 60000000),
|
|
|
|
|
|
|
|
|
"history_length": random.randint(1, 15),
|
|
|
|
|
|
|
|
|
"screen_avail_height": screen_config["height"] - random.randint(40, 80),
|
|
|
"screen_avail_width": screen_config["width"],
|
|
|
"screen_avail_left": 0,
|
|
|
"screen_avail_top": 0,
|
|
|
"screen_color_depth": 24,
|
|
|
"screen_height": screen_config["height"],
|
|
|
"screen_pixel_depth": 24,
|
|
|
"screen_width": screen_config["width"],
|
|
|
"screen_is_extended": random.choice([0, 1]),
|
|
|
"screen_orientation_angle": 0,
|
|
|
"screen_orientation_type": "landscape-primary",
|
|
|
|
|
|
|
|
|
"window_inner_width": min(screen_config["width"], random.randint(700, screen_config["width"])),
|
|
|
"window_inner_height": screen_config["height"] - random.randint(100, 200),
|
|
|
"window_outer_width": screen_config["width"],
|
|
|
"window_outer_height": screen_config["height"] - random.randint(40, 80),
|
|
|
"window_external_to_string": "[object External]",
|
|
|
|
|
|
|
|
|
"eval_to_string": "function eval() { [native code] }",
|
|
|
|
|
|
|
|
|
"apple_pay": 0,
|
|
|
|
|
|
|
|
|
"connection_rtt": connection_rtt,
|
|
|
"connection_downlink": connection_downlink,
|
|
|
"connection_save_data": 0,
|
|
|
"connection_effective_type": connection_effective_type,
|
|
|
|
|
|
|
|
|
"navigator_gpu_preferred_canvas_format": "bgra8unorm",
|
|
|
|
|
|
|
|
|
"virtual_keyboard_bounding_box": {
|
|
|
"x": 0, "y": 0, "width": 0, "height": 0,
|
|
|
"top": 0, "right": 0, "bottom": 0, "left": 0
|
|
|
},
|
|
|
|
|
|
|
|
|
"title_bar_area_bounding_box": {
|
|
|
"x": 0, "y": 0, "width": 0, "height": 0,
|
|
|
"top": 0, "right": 0, "bottom": 0, "left": 0
|
|
|
},
|
|
|
|
|
|
|
|
|
"navigator_can_load_ad_auction_fenced_frame": 0,
|
|
|
|
|
|
|
|
|
"video_frame_color_space": {
|
|
|
"full_range": True,
|
|
|
"matrix": "rgb",
|
|
|
"primaries": "bt709",
|
|
|
"transfer": "iec61966-2-1"
|
|
|
},
|
|
|
|
|
|
|
|
|
"supports_touch": 0,
|
|
|
|
|
|
|
|
|
"timezone_offset": lang_config["timezone_offset"],
|
|
|
|
|
|
|
|
|
"intl_date": {
|
|
|
"locale": lang_config["lang"],
|
|
|
"calendar": "gregory",
|
|
|
"numbering_system": "latn",
|
|
|
"time_zone": lang_config["timezone"],
|
|
|
"year": "numeric",
|
|
|
"month": "numeric",
|
|
|
"day": "numeric"
|
|
|
},
|
|
|
|
|
|
|
|
|
"intl_number": {
|
|
|
"locale": lang_config["lang"],
|
|
|
"numbering_system": "latn",
|
|
|
"style": "decimal",
|
|
|
"minimum_integer_digits": 1,
|
|
|
"minimum_fraction_digits": 0,
|
|
|
"maximum_fraction_digits": 3,
|
|
|
"use_grouping": "auto",
|
|
|
"notation": "standard",
|
|
|
"sign_display": "auto",
|
|
|
"rounding_increment": 1,
|
|
|
"rounding_mode": "halfExpand",
|
|
|
"rounding_priority": "auto",
|
|
|
"trailing_zero_display": "auto"
|
|
|
},
|
|
|
|
|
|
|
|
|
"prototype_hash": profile["hashes"]["prototype_hash"],
|
|
|
"math_hash": profile["hashes"]["math_hash"],
|
|
|
"architecture_test": 255,
|
|
|
"gpu_vendor": profile["gpu_vendor_string"],
|
|
|
"gpu_renderer": profile["gpu_renderer"],
|
|
|
"offline_audio_hash": profile["hashes"]["offline_audio_hash"],
|
|
|
"mime_types_hash": profile["hashes"]["mime_types_hash"],
|
|
|
"errors_hash": profile["hashes"]["errors_hash"],
|
|
|
|
|
|
|
|
|
"incognito": incognito,
|
|
|
|
|
|
|
|
|
"storage_quota": storage_quota,
|
|
|
"storage_usage": storage_usage,
|
|
|
|
|
|
|
|
|
"bluetooth": 1,
|
|
|
|
|
|
|
|
|
"battery_charging": battery_charging,
|
|
|
"battery_charging_time": 0 if battery_charging else random.randint(1000, 10000),
|
|
|
"battery_discharging_time": 999999999999 if battery_charging else random.randint(10000, 100000),
|
|
|
"battery_level": battery_level,
|
|
|
|
|
|
|
|
|
"xr_inline": 1,
|
|
|
|
|
|
|
|
|
"is_managed_configuration": 0,
|
|
|
|
|
|
|
|
|
"keyboard_layout": keyboard_layout_base,
|
|
|
|
|
|
|
|
|
"brands": brands,
|
|
|
"mobile": 0,
|
|
|
"architecture": profile["architecture"],
|
|
|
"bitness": profile["bitness"],
|
|
|
"form_factor": "null_string",
|
|
|
"full_version_list": full_version_list,
|
|
|
"model": "empty_string",
|
|
|
"platform_version": profile["os_version"] if browser_version["browser"] == "Chrome" else "19.0.0",
|
|
|
"ua_full_version": ua_full_version,
|
|
|
"wow_64": 0,
|
|
|
|
|
|
|
|
|
"permissions": permissions
|
|
|
}
|
|
|
|
|
|
return payload
|
|
|
|
|
|
async def _send_worker_request(self, session_id: str) -> bool:
|
|
|
"""
|
|
|
发送worker请求到Verisoul
|
|
|
"""
|
|
|
try:
|
|
|
|
|
|
worker_data = await self._generate_worker_payload(session_id)
|
|
|
|
|
|
|
|
|
cookie_parts = []
|
|
|
for name, value in self.async_client.cookies.items():
|
|
|
cookie_parts.append(f"{name}={value}")
|
|
|
|
|
|
worker_data['document_cookies'] = "; ".join(cookie_parts) if cookie_parts else ""
|
|
|
|
|
|
|
|
|
response = await self.async_client.post(
|
|
|
url="https://ingest.prod.verisoul.ai/worker",
|
|
|
json=worker_data,
|
|
|
headers={
|
|
|
"User-Agent": self.user_agent,
|
|
|
"Content-Type": "application/json",
|
|
|
"Origin": "https://app.warp.dev"
|
|
|
}
|
|
|
)
|
|
|
|
|
|
logger.info(f"Verisoul /worker 请求响应: {response.status_code}")
|
|
|
|
|
|
if response.status_code in [200, 201, 202, 204]:
|
|
|
logger.info("✅ Worker数据上报成功")
|
|
|
return True
|
|
|
else:
|
|
|
logger.error(f"Worker数据上报失败: {response.text}")
|
|
|
return False
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"Worker请求失败: {type(e).__name__}: {e}", exc_info=True)
|
|
|
return False
|
|
|
|
|
|
async def _get_public_ip(self) -> str:
|
|
|
"""使用当前会话的代理,访问 IP查询服务 来获取出口公网 IP。"""
|
|
|
try:
|
|
|
|
|
|
response = await self.async_client.get("https://api.ipify.org?format=json", timeout=10)
|
|
|
response.raise_for_status()
|
|
|
ip = response.json()["ip"]
|
|
|
logger.info(f"成功获取到出口公网 IP: {ip}")
|
|
|
return ip
|
|
|
except Exception as e:
|
|
|
logger.error(f"获取公网 IP 失败: {e}. 将使用一个随机IP作为备用。")
|
|
|
|
|
|
return f"{random.randint(1, 254)}.{random.randint(1, 254)}.{random.randint(1, 254)}.{random.randint(1, 254)}"
|
|
|
|
|
|
async def _generate_webrtc_sdp(self) -> str:
|
|
|
"""
|
|
|
动态生成一个高度仿真的 WebRTC SDP 字符串。
|
|
|
这个函数模仿了浏览器在创建 WebRTC 连接时生成的指纹。
|
|
|
"""
|
|
|
|
|
|
public_ip = await self._get_public_ip()
|
|
|
|
|
|
|
|
|
session_id_num = str(int(time.time() * 1000)) + str(random.randint(1000, 9999))
|
|
|
local_mdns_host = f"{uuid.uuid4()}.local"
|
|
|
ice_ufrag = secrets.token_urlsafe(4)
|
|
|
ice_pwd = secrets.token_urlsafe(24)
|
|
|
|
|
|
|
|
|
fingerprint_bytes = secrets.token_bytes(32)
|
|
|
fingerprint_str = ":".join(f"{b:02X}" for b in fingerprint_bytes)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sdp_lines = [
|
|
|
"v=0",
|
|
|
f"o=- {session_id_num} 2 IN IP4 127.0.0.1",
|
|
|
"s=-",
|
|
|
"t=0 0",
|
|
|
"a=group:BUNDLE 0 1 2",
|
|
|
"a=extmap-allow-mixed",
|
|
|
"a=msid-semantic: WMS",
|
|
|
|
|
|
|
|
|
f"m=audio {random.randint(10000, 60000)} UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126",
|
|
|
f"c=IN IP4 {public_ip}",
|
|
|
"a=rtcp:9 IN IP4 0.0.0.0",
|
|
|
|
|
|
f"a=candidate:{random.randint(1E9, 4E9)} 1 udp 2113937151 {local_mdns_host} {random.randint(10000, 60000)} typ host generation 0 network-cost 999",
|
|
|
|
|
|
f"a=candidate:{random.randint(1E9, 4E9)} 1 udp 1677729535 {public_ip} {random.randint(10000, 60000)} typ srflx raddr 0.0.0.0 rport 0 generation 0 network-cost 999",
|
|
|
f"a=ice-ufrag:{ice_ufrag}",
|
|
|
f"a=ice-pwd:{ice_pwd}",
|
|
|
f"a=fingerprint:sha-256 {fingerprint_str}",
|
|
|
"a=setup:actpass", "a=mid:0", "a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level", "a=recvonly",
|
|
|
"a=rtcp-mux",
|
|
|
*self._get_audio_codecs(),
|
|
|
|
|
|
|
|
|
f"m=video {random.randint(10000, 60000)} UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 123 127 121 125 107 108 109 35 36 124",
|
|
|
f"c=IN IP4 {public_ip}",
|
|
|
"a=rtcp:9 IN IP4 0.0.0.0",
|
|
|
f"a=candidate:{random.randint(1E9, 4E9)} 1 udp 2113937151 {local_mdns_host} {random.randint(10000, 60000)} typ host generation 0 network-cost 999",
|
|
|
f"a=candidate:{random.randint(1E9, 4E9)} 1 udp 1677729535 {public_ip} {random.randint(10000, 60000)} typ srflx raddr 0.0.0.0 rport 0 generation 0 network-cost 999",
|
|
|
f"a=ice-ufrag:{ice_ufrag}",
|
|
|
f"a=ice-pwd:{ice_pwd}",
|
|
|
f"a=fingerprint:sha-256 {fingerprint_str}",
|
|
|
"a=setup:actpass", "a=mid:1", "a=extmap:14 urn:ietf:params:rtp-hdrext:toffset", "a=recvonly", "a=rtcp-mux",
|
|
|
*self._get_video_codecs(),
|
|
|
|
|
|
|
|
|
f"m=application {random.randint(10000, 60000)} UDP/DTLS/SCTP webrtc-datachannel",
|
|
|
f"c=IN IP4 {public_ip}",
|
|
|
f"a=candidate:{random.randint(1E9, 4E9)} 1 udp 2113937151 {local_mdns_host} {random.randint(10000, 60000)} typ host generation 0 network-cost 999",
|
|
|
f"a=candidate:{random.randint(1E9, 4E9)} 1 udp 1677729535 {public_ip} {random.randint(10000, 60000)} typ srflx raddr 0.0.0.0 rport 0 generation 0 network-cost 999",
|
|
|
f"a=ice-ufrag:{ice_ufrag}",
|
|
|
f"a=ice-pwd:{ice_pwd}",
|
|
|
f"a=fingerprint:sha-256 {fingerprint_str}",
|
|
|
"a=setup:actpass", "a=mid:2", "a=sctp-port:5000", "a=max-message-size:262144"
|
|
|
]
|
|
|
|
|
|
|
|
|
return "\r\n".join(sdp_lines) + "\r\n"
|
|
|
|
|
|
def _get_audio_codecs(self):
|
|
|
|
|
|
return [
|
|
|
"a=rtpmap:111 opus/48000/2", "a=rtcp-fb:111 transport-cc", "a=fmtp:111 minptime=10;useinbandfec=1",
|
|
|
"a=rtpmap:103 G722/8000", "a=rtpmap:104 G722/8000", "a=rtpmap:9 G722/8000", "a=rtpmap:0 PCMU/8000",
|
|
|
"a=rtpmap:8 PCMA/8000", "a=rtpmap:106 CN/32000", "a=rtpmap:105 CN/16000", "a=rtpmap:13 CN/8000",
|
|
|
"a=rtpmap:110 telephone-event/48000", "a=rtpmap:112 telephone-event/32000",
|
|
|
"a=rtpmap:113 telephone-event/16000", "a=rtpmap:126 telephone-event/8000"
|
|
|
]
|
|
|
|
|
|
def _get_video_codecs(self):
|
|
|
|
|
|
return [
|
|
|
"a=rtpmap:96 VP8/90000", "a=rtcp-fb:96 goog-remb", "a=rtcp-fb:96 transport-cc", "a=rtcp-fb:96 ccm fir",
|
|
|
"a=rtcp-fb:96 nack", "a=rtcp-fb:96 nack pli",
|
|
|
"a=rtpmap:97 rtx/90000", "a=fmtp:97 apt=96",
|
|
|
"a=rtpmap:98 VP9/90000", "a=rtcp-fb:98 goog-remb", "a=rtcp-fb:98 transport-cc", "a=rtcp-fb:98 ccm fir",
|
|
|
"a=rtcp-fb:98 nack", "a=rtcp-fb:98 nack pli", "a=fmtp:98 profile-id=0",
|
|
|
"a=rtpmap:99 rtx/90000", "a=fmtp:99 apt=98",
|
|
|
"a=rtpmap:100 H264/90000", "a=rtcp-fb:100 goog-remb", "a=rtcp-fb:100 transport-cc", "a=rtcp-fb:100 ccm fir",
|
|
|
"a=rtcp-fb:100 nack", "a=rtcp-fb:100 nack pli",
|
|
|
"a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
|
|
|
"a=rtpmap:101 rtx/90000", "a=fmtp:101 apt=100",
|
|
|
"a=rtpmap:123 red/90000", "a=rtpmap:127 ulpfec/90000"
|
|
|
]
|
|
|
|
|
|
async def register_account(self, temp_mail_client: TempMailAPIClient) -> Optional[str]:
|
|
|
"""执行完整的注册流程"""
|
|
|
|
|
|
email_result = await temp_mail_client.generate_email()
|
|
|
if not email_result.get('success'):
|
|
|
logger.error(f"生成邮箱失败: {email_result.get('error')}")
|
|
|
return None
|
|
|
|
|
|
email = email_result['email']
|
|
|
logger.info(f"✅ 使用邮箱: {email}")
|
|
|
|
|
|
|
|
|
attempt_modes = [] |
|
|
if config.PROXY_URL: |
|
|
attempt_modes.extend(["proxy"] * max(config.MAX_PROXY_RETRIES, 1)) |
|
|
attempt_modes.append("direct") |
|
|
|
|
|
for attempt_index, mode in enumerate(attempt_modes, start=1): |
|
|
proxy_str = None |
|
|
proxy = None |
|
|
|
|
|
if mode == "proxy": |
|
|
proxy_str = await self.proxy_manager.get_proxy() |
|
|
proxy = self.proxy_manager.format_proxy_for_httpx(proxy_str) if proxy_str else None |
|
|
|
|
|
if proxy: |
|
|
logger.info(f"[{email}] 第{attempt_index}次尝试,使用代理: {proxy_str}") |
|
|
else: |
|
|
logger.info(f"[{email}] 第{attempt_index}次尝试,代理不可用,改为直连模式") |
|
|
else: |
|
|
logger.info(f"[{email}] 第{attempt_index}次尝试,使用直连模式") |
|
|
|
|
|
|
|
|
self.async_client = httpx.AsyncClient( |
|
|
proxy=proxy, |
|
|
|
|
|
verify=False,
|
|
|
timeout=httpx.Timeout(60.0),
|
|
|
cookies=httpx.Cookies()
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
session_id = str(uuid.uuid4())
|
|
|
logger.info(f"[{email}] Verisoul 流程开始, Session ID: {session_id}")
|
|
|
|
|
|
|
|
|
logger.info(f"[{email}] 发送worker数据...")
|
|
|
worker_success = await self._send_worker_request(session_id)
|
|
|
if not worker_success:
|
|
|
logger.warning("Worker数据上报失败,但继续尝试...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"发送登录请求: {email}")
|
|
|
signin_result = await self.send_email_signin_request(email, proxy)
|
|
|
|
|
|
|
|
|
if not signin_result["success"]:
|
|
|
if signin_result.get("error") == "proxy_error":
|
|
|
logger.warning(f"代理错误,更换代理重试...")
|
|
|
continue
|
|
|
else:
|
|
|
logger.error(f"发送登录请求失败: {signin_result.get('error')}")
|
|
|
continue
|
|
|
|
|
|
|
|
|
logger.info("等待验证邮件...")
|
|
|
await asyncio.sleep(5)
|
|
|
|
|
|
verification_result = await self.wait_for_verification_email(
|
|
|
temp_mail_client=temp_mail_client,
|
|
|
email=email
|
|
|
)
|
|
|
|
|
|
if not verification_result:
|
|
|
logger.error("未收到验证邮件")
|
|
|
continue
|
|
|
|
|
|
oob_code = verification_result.get('oob_code')
|
|
|
if not oob_code:
|
|
|
logger.error("未能提取验证码")
|
|
|
continue
|
|
|
|
|
|
verification_link = verification_result.get('verification_link')
|
|
|
if not verification_link:
|
|
|
logger.error("未能提取验证链接")
|
|
|
continue
|
|
|
|
|
|
|
|
|
logger.info("完成登录...")
|
|
|
complete_result = await self.complete_email_signin(email, oob_code)
|
|
|
|
|
|
if not complete_result["success"]:
|
|
|
if complete_result.get("error") == "proxy_error":
|
|
|
logger.warning(f"代理错误,更换代理重试...")
|
|
|
continue
|
|
|
else:
|
|
|
logger.error(f"完成登录失败: {complete_result.get('error')}")
|
|
|
continue
|
|
|
|
|
|
|
|
|
logger.info("激活Warp用户...")
|
|
|
activation_result = await self.activate_warp_user(complete_result["id_token"], session_id)
|
|
|
|
|
|
if not activation_result["success"]:
|
|
|
logger.error(f"[{email}] 账号因违反服务条款被标记,跳过此账号")
|
|
|
return None
|
|
|
|
|
|
request_limit_result = await self._get_request_limit(complete_result["id_token"])
|
|
|
|
|
|
|
|
|
await self.db_manager.add_account(
|
|
|
email=email,
|
|
|
local_id=complete_result["local_id"],
|
|
|
id_token=complete_result["id_token"],
|
|
|
refresh_token=complete_result["refresh_token"],
|
|
|
status='active',
|
|
|
proxy_info=proxy_str,
|
|
|
user_agent=self.user_agent
|
|
|
)
|
|
|
|
|
|
logger.info(f"✅ 注册成功: {email}")
|
|
|
return complete_result["local_id"]
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"注册过程出错: {e}") |
|
|
if attempt_index < len(attempt_modes): |
|
|
if proxy: |
|
|
logger.info("将更换代理或切换网络方式重试...") |
|
|
else: |
|
|
logger.info("将重新尝试直连...") |
|
|
await asyncio.sleep(2) |
|
|
continue |
|
|
|
|
|
logger.error(f"[{email}] 尝试全部网络方式后仍然失败") |
|
|
return None |
|
|
|
|
|
async def _get_user_info(self, id_token: str) -> Dict[str, Any]:
|
|
|
"""获取账户请求额度
|
|
|
|
|
|
调用 GetUser 接口获取账户信息,通过 billingMetadata 判断额度
|
|
|
billingMetadata 为 null → 150 额度
|
|
|
billingMetadata 不为 null → 2500 额度
|
|
|
|
|
|
Args:
|
|
|
id_token: Firebase ID Token
|
|
|
|
|
|
Returns:
|
|
|
包含额度信息的字典
|
|
|
"""
|
|
|
if not id_token:
|
|
|
return {"success": False, "error": "缺少Firebase ID Token"}
|
|
|
|
|
|
try:
|
|
|
url = "https://app.warp.dev/graphql/v2"
|
|
|
|
|
|
|
|
|
|
|
|
query = """
|
|
|
query GetUser($requestContext: RequestContext!) {
|
|
|
user(requestContext: $requestContext) {
|
|
|
__typename
|
|
|
... on UserOutput {
|
|
|
user {
|
|
|
billingMetadata {
|
|
|
__typename
|
|
|
}
|
|
|
profile {
|
|
|
email
|
|
|
uid
|
|
|
}
|
|
|
isOnboarded
|
|
|
}
|
|
|
}
|
|
|
... on UserFacingError {
|
|
|
error {
|
|
|
message
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
"""
|
|
|
|
|
|
|
|
|
import platform
|
|
|
import uuid
|
|
|
os_name = "Windows"
|
|
|
os_version = "10 (19045)"
|
|
|
os_category = "Windows"
|
|
|
|
|
|
data = {
|
|
|
"operationName": "GetUser",
|
|
|
"variables": {
|
|
|
"requestContext": {
|
|
|
"clientContext": {
|
|
|
"version": "v0.2025.09.10.08.11.stable_01"
|
|
|
},
|
|
|
"osContext": {
|
|
|
"category": os_category,
|
|
|
"linuxKernelVersion": None,
|
|
|
"name": os_name,
|
|
|
"version": os_version
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
"query": query
|
|
|
}
|
|
|
|
|
|
headers = {
|
|
|
"Content-Type": "application/json",
|
|
|
"authorization": f"Bearer {id_token}",
|
|
|
"x-warp-client-version": "v0.2025.09.10.08.11.stable_01",
|
|
|
"x-warp-os-category": "Windows",
|
|
|
"x-warp-os-name": "Windows",
|
|
|
"x-warp-os-version": "10 (19045)",
|
|
|
"X-warp-experiment-id": str(uuid.uuid4())
|
|
|
}
|
|
|
|
|
|
print("📊 调用GetUse接口...")
|
|
|
|
|
|
response = await self.async_client.post(
|
|
|
url,
|
|
|
params={"op": "GetUser"},
|
|
|
json=data,
|
|
|
headers=headers,
|
|
|
)
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
result = response.json()
|
|
|
|
|
|
|
|
|
if "errors" in result:
|
|
|
error_msg = result["errors"][0].get("message", "Unknown error")
|
|
|
print(f"❌ GraphQL错误: {error_msg}")
|
|
|
return {"success": False, "error": error_msg}
|
|
|
|
|
|
|
|
|
data_result = result.get("data", {})
|
|
|
user_data = data_result.get("user", {})
|
|
|
|
|
|
if user_data.get("__typename") == "UserOutput":
|
|
|
user_info = user_data.get("user", {})
|
|
|
billing_metadata = user_info.get("billingMetadata")
|
|
|
profile = user_info.get("profile", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if billing_metadata is None:
|
|
|
request_limit = 150
|
|
|
quota_type = "📋 普通额度"
|
|
|
else:
|
|
|
request_limit = 2500
|
|
|
quota_type = "🎉 高额度"
|
|
|
|
|
|
email = profile.get("email", "N/A")
|
|
|
uid = profile.get("uid", "N/A")
|
|
|
|
|
|
print(f"✅ 账户额度信息:")
|
|
|
print(f" 📧 邮箱: {email}")
|
|
|
print(f" 🎯 UID: {uid}")
|
|
|
print(f" {quota_type}: {request_limit}")
|
|
|
print(f" 📊 billingMetadata: {'null' if billing_metadata is None else 'exists'}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"requestLimit": request_limit,
|
|
|
"quotaType": "high" if request_limit == 2500 else "normal",
|
|
|
"email": email,
|
|
|
"uid": uid,
|
|
|
"hasBillingMetadata": billing_metadata is not None
|
|
|
}
|
|
|
elif user_data.get("__typename") == "UserFacingError":
|
|
|
error = user_data.get("error", {}).get("message", "Unknown error")
|
|
|
print(f"❌ 获取额度失败: {error}")
|
|
|
return {"success": False, "error": error}
|
|
|
else:
|
|
|
print(f"❌ 响应中没有找到用户信息")
|
|
|
return {"success": False, "error": "未找到用户信息"}
|
|
|
else:
|
|
|
error_text = response.text[:500]
|
|
|
print(f"❌ HTTP错误 {response.status_code}")
|
|
|
return {"success": False, "error": f"HTTP {response.status_code}: {error_text}"}
|
|
|
|
|
|
except Exception as e:
|
|
|
print(f"❌ 获取额度错误: {e}")
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
async def _get_request_limit(self, id_token: str) -> Dict[str, Any]:
|
|
|
"""获取账户请求额度
|
|
|
|
|
|
调用 GetUser 接口获取账户信息,通过 billingMetadata 判断额度
|
|
|
billingMetadata 为 null → 150 额度
|
|
|
billingMetadata 不为 null → 2500 额度
|
|
|
|
|
|
Args:
|
|
|
id_token: Firebase ID Token
|
|
|
|
|
|
Returns:
|
|
|
包含额度信息的字典
|
|
|
"""
|
|
|
|
|
|
if not id_token:
|
|
|
return {"success": False, "error": "缺少Firebase ID Token"}
|
|
|
|
|
|
try:
|
|
|
url = "https://app.warp.dev/graphql/v2"
|
|
|
|
|
|
|
|
|
|
|
|
query = """query GetRequestLimitInfo($requestContext: RequestContext!) {\n user(requestContext: $requestContext) {\n __typename\n ... on UserOutput {\n user {\n requestLimitInfo {\n isUnlimited\n nextRefreshTime\n requestLimit\n requestsUsedSinceLastRefresh\n requestLimitRefreshDuration\n isUnlimitedAutosuggestions\n acceptedAutosuggestionsLimit\n acceptedAutosuggestionsSinceLastRefresh\n isUnlimitedVoice\n voiceRequestLimit\n voiceRequestsUsedSinceLastRefresh\n voiceTokenLimit\n voiceTokensUsedSinceLastRefresh\n isUnlimitedCodebaseIndices\n maxCodebaseIndices\n maxFilesPerRepo\n embeddingGenerationBatchSize\n }\n }\n }\n ... on UserFacingError {\n error {\n __typename\n ... on SharedObjectsLimitExceeded {\n limit\n objectType\n message\n }\n ... on PersonalObjectsLimitExceeded {\n limit\n objectType\n message\n }\n ... on AccountDelinquencyError {\n message\n }\n ... on GenericStringObjectUniqueKeyConflict {\n message\n }\n }\n responseContext {\n serverVersion\n }\n }\n }\n}\n"""
|
|
|
|
|
|
|
|
|
import platform
|
|
|
import uuid
|
|
|
os_category = "Web"
|
|
|
os_name = "Windows"
|
|
|
os_version = "NT 10.0"
|
|
|
app_version = "v0.2025.10.01.08.12.stable_02"
|
|
|
|
|
|
data = {
|
|
|
"operationName": "GetRequestLimitInfo",
|
|
|
"variables": {
|
|
|
"requestContext": {
|
|
|
"clientContext": {
|
|
|
"version": app_version
|
|
|
},
|
|
|
"osContext": {
|
|
|
"category": os_category,
|
|
|
"linuxKernelVersion": None,
|
|
|
"name": os_name,
|
|
|
"version": os_version
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
"query": query
|
|
|
}
|
|
|
|
|
|
headers = {
|
|
|
"Content-Type": "application/json",
|
|
|
"authorization": f"Bearer {id_token}",
|
|
|
"x-warp-client-id": "warp-app",
|
|
|
"x-warp-client-version": app_version,
|
|
|
"x-warp-os-category": os_category,
|
|
|
"x-warp-os-name": os_name,
|
|
|
"x-warp-os-version": os_version,
|
|
|
}
|
|
|
|
|
|
print("📊 调用GetRequestLimitInfo接口...")
|
|
|
|
|
|
response = await self.async_client.post(
|
|
|
url,
|
|
|
params={"op": "GetRequestLimitInfo"},
|
|
|
json=data,
|
|
|
headers=headers,
|
|
|
)
|
|
|
|
|
|
if response.status_code == 200:
|
|
|
result = response.json()
|
|
|
|
|
|
|
|
|
if "errors" in result:
|
|
|
error_msg = result["errors"][0].get("message", "Unknown error")
|
|
|
print(f"❌ GraphQL错误: {error_msg}")
|
|
|
return {"success": False, "error": error_msg}
|
|
|
|
|
|
|
|
|
data_result = result.get("data", {})
|
|
|
user_data = data_result.get("user", {})
|
|
|
|
|
|
if user_data.get("__typename") == "UserOutput":
|
|
|
user_info = user_data.get("user", {})
|
|
|
request_limit_info = user_info.get("requestLimitInfo", {})
|
|
|
|
|
|
|
|
|
request_limit = request_limit_info.get("requestLimit", 0)
|
|
|
requests_used = request_limit_info.get("requestsUsedSinceLastRefresh", 0)
|
|
|
is_unlimited = request_limit_info.get("isUnlimited", False)
|
|
|
next_refresh_time = request_limit_info.get("nextRefreshTime", "N/A")
|
|
|
refresh_duration = request_limit_info.get("requestLimitRefreshDuration", "WEEKLY")
|
|
|
|
|
|
|
|
|
requests_remaining = request_limit - requests_used
|
|
|
|
|
|
|
|
|
if is_unlimited:
|
|
|
quota_type = "🚀 无限额度"
|
|
|
elif request_limit >= 2500:
|
|
|
quota_type = "🎉 高额度"
|
|
|
else:
|
|
|
quota_type = "📋 普通额度"
|
|
|
|
|
|
print(f"✅ 账户额度信息:")
|
|
|
print(f" {quota_type}: {request_limit}")
|
|
|
print(f" 📊 已使用: {requests_used}/{request_limit}")
|
|
|
print(f" 💎 剩余: {requests_remaining}")
|
|
|
print(f" 🔄 刷新周期: {refresh_duration}")
|
|
|
print(f" ⏰ 下次刷新: {next_refresh_time}")
|
|
|
|
|
|
|
|
|
if request_limit_info.get("isUnlimitedAutosuggestions"):
|
|
|
print(f" ✨ 自动建议: 无限制")
|
|
|
if request_limit_info.get("maxCodebaseIndices"):
|
|
|
print(f" 📚 最大代码库索引: {request_limit_info.get('maxCodebaseIndices')}")
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"requestLimit": request_limit,
|
|
|
"requestsUsed": requests_used,
|
|
|
"requestsRemaining": requests_remaining,
|
|
|
"isUnlimited": is_unlimited,
|
|
|
"nextRefreshTime": next_refresh_time,
|
|
|
"refreshDuration": refresh_duration,
|
|
|
"quotaType": "unlimited" if is_unlimited else ("high" if request_limit >= 2500 else "normal"),
|
|
|
|
|
|
"autosuggestions": {
|
|
|
"isUnlimited": request_limit_info.get("isUnlimitedAutosuggestions", False),
|
|
|
"limit": request_limit_info.get("acceptedAutosuggestionsLimit", 0),
|
|
|
"used": request_limit_info.get("acceptedAutosuggestionsSinceLastRefresh", 0)
|
|
|
},
|
|
|
"voice": {
|
|
|
"isUnlimited": request_limit_info.get("isUnlimitedVoice", False),
|
|
|
"requestLimit": request_limit_info.get("voiceRequestLimit", 0),
|
|
|
"requestsUsed": request_limit_info.get("voiceRequestsUsedSinceLastRefresh", 0),
|
|
|
"tokenLimit": request_limit_info.get("voiceTokenLimit", 0),
|
|
|
"tokensUsed": request_limit_info.get("voiceTokensUsedSinceLastRefresh", 0)
|
|
|
},
|
|
|
"codebase": {
|
|
|
"isUnlimited": request_limit_info.get("isUnlimitedCodebaseIndices", False),
|
|
|
"maxIndices": request_limit_info.get("maxCodebaseIndices", 0),
|
|
|
"maxFilesPerRepo": request_limit_info.get("maxFilesPerRepo", 0)
|
|
|
}
|
|
|
}
|
|
|
elif user_data.get("__typename") == "UserFacingError":
|
|
|
error = user_data.get("error", {}).get("message", "Unknown error")
|
|
|
print(f"❌ 获取额度失败: {error}")
|
|
|
return {"success": False, "error": error}
|
|
|
else:
|
|
|
print(f"❌ 响应中没有找到用户信息")
|
|
|
return {"success": False, "error": "未找到用户信息"}
|
|
|
else:
|
|
|
error_text = response.text[:500]
|
|
|
print(f"❌ HTTP错误 {response.status_code}")
|
|
|
return {"success": False, "error": f"HTTP {response.status_code}: {error_text}"}
|
|
|
|
|
|
except Exception as e:
|
|
|
print(f"❌ 获取额度错误: {e}")
|
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
class RegistrationMonitor:
|
|
|
"""注册监控器"""
|
|
|
|
|
|
def __init__(self, target_accounts: int = config.TARGET_ACCOUNTS, max_concurrent: int = config.MAX_CONCURRENT_REGISTER):
|
|
|
self.target_accounts = target_accounts
|
|
|
self.max_concurrent = max_concurrent
|
|
|
self.db_manager = AsyncDatabaseManager()
|
|
|
self.proxy_manager = AsyncProxyManager()
|
|
|
self.running = False
|
|
|
self.stats = {
|
|
|
"total_attempts": 0,
|
|
|
"successful": 0,
|
|
|
"failed": 0
|
|
|
}
|
|
|
self.stats_lock = asyncio.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
async def registration_worker(self, worker_id: int):
|
|
|
"""异步注册工作函数"""
|
|
|
logger.info(f"🚀 工作线程 {worker_id} 已启动")
|
|
|
|
|
|
while self.running:
|
|
|
try:
|
|
|
|
|
|
current_count = await self.db_manager.get_account_count('active')
|
|
|
if current_count >= self.target_accounts:
|
|
|
logger.info(f"[Worker-{worker_id}] 已达到目标账户数 ({current_count}/{self.target_accounts})")
|
|
|
await asyncio.sleep(30)
|
|
|
continue
|
|
|
|
|
|
|
|
|
temp_mail_client = TempMailAPIClient()
|
|
|
async with temp_mail_client:
|
|
|
|
|
|
bot = WarpRegistrationBot(self.db_manager, self.proxy_manager)
|
|
|
local_id = await bot.register_account(temp_mail_client)
|
|
|
|
|
|
email = temp_mail_client.current_email or "unknown"
|
|
|
|
|
|
async with self.stats_lock:
|
|
|
self.stats["total_attempts"] += 1
|
|
|
if local_id:
|
|
|
self.stats["successful"] += 1
|
|
|
else:
|
|
|
self.stats["failed"] += 1
|
|
|
|
|
|
if local_id:
|
|
|
logger.info(f"[Worker-{worker_id}] ✅ 注册成功: {email}")
|
|
|
await asyncio.sleep(5)
|
|
|
else:
|
|
|
logger.error(f"[Worker-{worker_id}] ❌ 注册失败: {email}")
|
|
|
await asyncio.sleep(30)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.error(f"[Worker-{worker_id}] 工作线程异常: {e}")
|
|
|
async with self.stats_lock:
|
|
|
self.stats["failed"] += 1
|
|
|
await asyncio.sleep(10)
|
|
|
|
|
|
logger.info(f"🛑 工作线程 {worker_id} 已停止")
|
|
|
|
|
|
async def print_stats(self):
|
|
|
"""定期打印统计信息"""
|
|
|
while self.running:
|
|
|
await asyncio.sleep(30)
|
|
|
|
|
|
async with self.stats_lock:
|
|
|
stats = self.stats.copy()
|
|
|
|
|
|
active_count = await self.db_manager.get_account_count('active')
|
|
|
|
|
|
logger.info("=" * 50)
|
|
|
logger.info(f"📊 注册统计")
|
|
|
logger.info(f"🎯 目标账户数: {self.target_accounts}")
|
|
|
logger.info(f"✅ 当前活跃账户: {active_count}")
|
|
|
logger.info(
|
|
|
f"📈 进度: {active_count}/{self.target_accounts} ({active_count / self.target_accounts * 100:.1f}%)")
|
|
|
logger.info(f"🔄 总尝试次数: {stats['total_attempts']}")
|
|
|
logger.info(f"✅ 成功: {stats['successful']}")
|
|
|
logger.info(f"❌ 失败: {stats['failed']}")
|
|
|
if stats['total_attempts'] > 0:
|
|
|
success_rate = stats['successful'] / stats['total_attempts'] * 100
|
|
|
logger.info(f"📊 成功率: {success_rate:.1f}%")
|
|
|
logger.info("=" * 50)
|
|
|
|
|
|
async def start(self):
|
|
|
"""启动监控器"""
|
|
|
self.running = True
|
|
|
|
|
|
|
|
|
tasks = []
|
|
|
for i in range(self.max_concurrent):
|
|
|
task = asyncio.create_task(self.registration_worker(i + 1))
|
|
|
tasks.append(task)
|
|
|
|
|
|
|
|
|
stats_task = asyncio.create_task(self.print_stats())
|
|
|
tasks.append(stats_task)
|
|
|
|
|
|
logger.info(f"✅ 启动了 {self.max_concurrent} 个工作线程")
|
|
|
|
|
|
try:
|
|
|
await asyncio.gather(*tasks)
|
|
|
except KeyboardInterrupt:
|
|
|
logger.info("⌨️ 收到停止信号")
|
|
|
finally:
|
|
|
self.running = False
|
|
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
"""主函数"""
|
|
|
logger.info("=" * 60)
|
|
|
logger.info("🚀 Warp账号注册脚本启动")
|
|
|
logger.info(f"📊 目标账号数: {config.TARGET_ACCOUNTS}")
|
|
|
logger.info(f"⚡ 最大并发数: {config.MAX_CONCURRENT_REGISTER}")
|
|
|
logger.info("=" * 60)
|
|
|
|
|
|
monitor = RegistrationMonitor(
|
|
|
target_accounts=config.TARGET_ACCOUNTS,
|
|
|
max_concurrent=config.MAX_CONCURRENT_REGISTER
|
|
|
)
|
|
|
|
|
|
await monitor.start()
|
|
|
|
|
|
logger.info("✅ 注册任务完成")
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
asyncio.run(main())
|
|
|
|