重构机器人
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -38,4 +38,5 @@ Thumbs.db
|
|||||||
|
|
||||||
/.gemini
|
/.gemini
|
||||||
/docs
|
/docs
|
||||||
/reference
|
/reference
|
||||||
|
/outputs
|
||||||
9
bot/__init__.py
Normal file
9
bot/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
Telegram Bot 模块
|
||||||
|
|
||||||
|
提供 Telegram Bot 接口来控制 OpenAI 账号注册系统
|
||||||
|
"""
|
||||||
|
|
||||||
|
from bot.app import create_application
|
||||||
|
|
||||||
|
__all__ = ["create_application"]
|
||||||
63
bot/app.py
Normal file
63
bot/app.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
Telegram Bot Application 创建模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from telegram.ext import Application, CommandHandler
|
||||||
|
|
||||||
|
from config import AppConfig
|
||||||
|
from bot.handlers import start, go, status, settings
|
||||||
|
|
||||||
|
|
||||||
|
def create_application(config: AppConfig) -> Application:
|
||||||
|
"""
|
||||||
|
创建并配置 Telegram Bot Application
|
||||||
|
|
||||||
|
参数:
|
||||||
|
config: AppConfig 配置对象
|
||||||
|
|
||||||
|
返回:
|
||||||
|
配置好的 Application 实例
|
||||||
|
"""
|
||||||
|
if not config.telegram_bot_token:
|
||||||
|
raise ValueError("TELEGRAM_BOT_TOKEN is not configured")
|
||||||
|
|
||||||
|
# 创建 Application
|
||||||
|
application = Application.builder().token(config.telegram_bot_token).build()
|
||||||
|
|
||||||
|
# 存储配置到 bot_data 供 handlers 使用
|
||||||
|
application.bot_data["config"] = config
|
||||||
|
|
||||||
|
# 解析允许的用户列表
|
||||||
|
allowed_users = set()
|
||||||
|
if config.telegram_allowed_users:
|
||||||
|
for uid in config.telegram_allowed_users.split(","):
|
||||||
|
uid = uid.strip()
|
||||||
|
if uid.isdigit():
|
||||||
|
allowed_users.add(int(uid))
|
||||||
|
|
||||||
|
admin_users = set()
|
||||||
|
if config.telegram_admin_users:
|
||||||
|
for uid in config.telegram_admin_users.split(","):
|
||||||
|
uid = uid.strip()
|
||||||
|
if uid.isdigit():
|
||||||
|
admin_users.add(int(uid))
|
||||||
|
|
||||||
|
application.bot_data["allowed_users"] = allowed_users
|
||||||
|
application.bot_data["admin_users"] = admin_users
|
||||||
|
|
||||||
|
# 初始化用户设置
|
||||||
|
application.bot_data["user_settings"] = {}
|
||||||
|
|
||||||
|
# 初始化任务管理器
|
||||||
|
from bot.services.task_manager import TaskManager
|
||||||
|
|
||||||
|
application.bot_data["task_manager"] = TaskManager()
|
||||||
|
|
||||||
|
# 注册命令处理器
|
||||||
|
application.add_handler(CommandHandler("start", start.start_command))
|
||||||
|
application.add_handler(CommandHandler("help", start.help_command))
|
||||||
|
application.add_handler(CommandHandler("go", go.go_command))
|
||||||
|
application.add_handler(CommandHandler("status", status.status_command))
|
||||||
|
application.add_handler(CommandHandler("set", settings.set_command))
|
||||||
|
|
||||||
|
return application
|
||||||
7
bot/handlers/__init__.py
Normal file
7
bot/handlers/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Bot 命令处理器模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from bot.handlers import start, go, status, settings
|
||||||
|
|
||||||
|
__all__ = ["start", "go", "status", "settings"]
|
||||||
290
bot/handlers/go.py
Normal file
290
bot/handlers/go.py
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
"""
|
||||||
|
/go 命令处理器 - 生成支付链接并发送 JSON 文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from bot.middlewares.auth import require_auth
|
||||||
|
from bot.services.task_manager import TaskStatus
|
||||||
|
from bot.services.file_sender import send_results_as_json
|
||||||
|
from bot.handlers.settings import get_user_settings, DEFAULT_WORKERS
|
||||||
|
|
||||||
|
|
||||||
|
PLAN_MAPPING = {
|
||||||
|
"plus": "chatgptplusplan",
|
||||||
|
"pro": "chatgptproplan",
|
||||||
|
"chatgptplusplan": "chatgptplusplan",
|
||||||
|
"chatgptproplan": "chatgptproplan",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@require_auth
|
||||||
|
async def go_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""
|
||||||
|
处理 /go 命令 - 注册账号并生成支付链接
|
||||||
|
|
||||||
|
用法:
|
||||||
|
/go - 生成 1 个 Plus 支付链接
|
||||||
|
/go 5 - 生成 5 个 Plus 支付链接
|
||||||
|
/go 5 plus - 生成 5 个 Plus 支付链接
|
||||||
|
/go 3 pro - 生成 3 个 Pro 支付链接
|
||||||
|
"""
|
||||||
|
# 解析参数
|
||||||
|
num_accounts = 1
|
||||||
|
plan_input = "plus"
|
||||||
|
|
||||||
|
if context.args:
|
||||||
|
# 第一个参数:数量
|
||||||
|
try:
|
||||||
|
num_accounts = int(context.args[0])
|
||||||
|
if num_accounts < 1:
|
||||||
|
num_accounts = 1
|
||||||
|
elif num_accounts > 10:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"⚠️ 单次最多处理 10 个账号,已自动调整为 10"
|
||||||
|
)
|
||||||
|
num_accounts = 10
|
||||||
|
except ValueError:
|
||||||
|
# 可能第一个参数是 plan
|
||||||
|
if context.args[0].lower() in PLAN_MAPPING:
|
||||||
|
plan_input = context.args[0].lower()
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"❌ 无效参数\n\n"
|
||||||
|
"用法: `/go [数量] [plan]`\n"
|
||||||
|
"示例:\n"
|
||||||
|
"• `/go` - 1个 Plus 计划\n"
|
||||||
|
"• `/go 5` - 5个 Plus 计划\n"
|
||||||
|
"• `/go 3 pro` - 3个 Pro 计划",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 第二个参数:plan
|
||||||
|
if len(context.args) > 1:
|
||||||
|
plan_input = context.args[1].lower()
|
||||||
|
|
||||||
|
# 验证 plan
|
||||||
|
plan_name = PLAN_MAPPING.get(plan_input)
|
||||||
|
if not plan_name:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"❌ 无效的订阅计划: `{plan_input}`\n\n" f"可选值: `plus`, `pro`",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
plan_display = "Plus" if "plus" in plan_name else "Pro"
|
||||||
|
|
||||||
|
# 获取配置和任务管理器
|
||||||
|
config = context.bot_data["config"]
|
||||||
|
task_manager = context.bot_data["task_manager"]
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
|
||||||
|
# 获取用户设置的并发数
|
||||||
|
user_settings = get_user_settings(context, user_id)
|
||||||
|
workers = user_settings.get("workers", DEFAULT_WORKERS)
|
||||||
|
|
||||||
|
# 创建任务
|
||||||
|
task_id = task_manager.create_task(
|
||||||
|
user_id=user_id,
|
||||||
|
task_type="go",
|
||||||
|
total=num_accounts,
|
||||||
|
description=f"{num_accounts} x {plan_display} 支付链接",
|
||||||
|
)
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"🚀 **开始生成支付链接**\n\n"
|
||||||
|
f"📦 计划: {plan_display}\n"
|
||||||
|
f"📊 数量: {num_accounts}\n"
|
||||||
|
f"👷 并发: {workers}\n"
|
||||||
|
f"📋 任务 ID: `{task_id}`\n\n"
|
||||||
|
f"使用 `/status {task_id}` 查看进度",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 在后台执行工作流
|
||||||
|
asyncio.create_task(
|
||||||
|
_run_go_workflow(
|
||||||
|
config, task_manager, task_id, num_accounts, plan_name, workers, update, context
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _run_go_workflow(
|
||||||
|
config,
|
||||||
|
task_manager,
|
||||||
|
task_id: str,
|
||||||
|
num_accounts: int,
|
||||||
|
plan_name: str,
|
||||||
|
workers: int,
|
||||||
|
update: Update,
|
||||||
|
context: ContextTypes.DEFAULT_TYPE,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
后台执行工作流:并发注册 + 登录 + 获取支付链接
|
||||||
|
"""
|
||||||
|
plan_display = "Plus" if "plus" in plan_name else "Pro"
|
||||||
|
semaphore = asyncio.Semaphore(workers)
|
||||||
|
|
||||||
|
async def process_one(index: int):
|
||||||
|
async with semaphore:
|
||||||
|
account_num = index + 1
|
||||||
|
try:
|
||||||
|
task_manager.update_progress(
|
||||||
|
task_id,
|
||||||
|
current_item=f"账号 {account_num}/{num_accounts}",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await _register_with_checkout(config, account_num, plan_name)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"failed_stage": "exception",
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 并发执行所有任务
|
||||||
|
tasks = [process_one(i) for i in range(num_accounts)]
|
||||||
|
results = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
# 更新进度
|
||||||
|
success_count = sum(1 for r in results if r.get("status") == "success")
|
||||||
|
failed_count = len(results) - success_count
|
||||||
|
|
||||||
|
task_manager.update_progress(task_id, completed=num_accounts)
|
||||||
|
|
||||||
|
# 完成任务
|
||||||
|
task_manager.complete_task(
|
||||||
|
task_id,
|
||||||
|
status=TaskStatus.COMPLETED if failed_count == 0 else TaskStatus.PARTIAL,
|
||||||
|
result={
|
||||||
|
"success": success_count,
|
||||||
|
"failed": failed_count,
|
||||||
|
"total": num_accounts,
|
||||||
|
"plan": plan_display,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送 JSON 文件结果
|
||||||
|
await send_results_as_json(
|
||||||
|
update=update,
|
||||||
|
context=context,
|
||||||
|
task_id=task_id,
|
||||||
|
plan=plan_display,
|
||||||
|
results=results,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _register_with_checkout(config, task_id: int, plan_name: str):
|
||||||
|
"""
|
||||||
|
执行完整的注册 + 登录 + checkout 流程
|
||||||
|
|
||||||
|
返回包含所有信息的结果字典
|
||||||
|
"""
|
||||||
|
from core.session import OAISession
|
||||||
|
from core.flow import RegisterFlow
|
||||||
|
from core.login_flow import LoginFlow
|
||||||
|
from core.checkout import CheckoutFlow
|
||||||
|
from utils.logger import logger
|
||||||
|
import re
|
||||||
|
|
||||||
|
def _mask_proxy(proxy: str) -> str:
|
||||||
|
return re.sub(r"://([^:]+):([^@]+)@", r"://***:***@", proxy)
|
||||||
|
|
||||||
|
# 选择代理
|
||||||
|
proxy = config.proxy.get_next_proxy()
|
||||||
|
if proxy:
|
||||||
|
logger.info(f"[Go {task_id}] Using proxy: {_mask_proxy(proxy)}")
|
||||||
|
|
||||||
|
session = None
|
||||||
|
try:
|
||||||
|
session = OAISession(proxy=proxy, impersonate=config.tls_impersonate)
|
||||||
|
|
||||||
|
# Step 1: 注册
|
||||||
|
logger.info(f"[Go {task_id}] Step 1: Registering...")
|
||||||
|
flow = RegisterFlow(session, config)
|
||||||
|
reg_result = await flow.run()
|
||||||
|
|
||||||
|
if reg_result.get("status") != "success":
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"failed_stage": "register",
|
||||||
|
"error": reg_result.get("error", "Registration failed"),
|
||||||
|
}
|
||||||
|
|
||||||
|
email = reg_result.get("email")
|
||||||
|
password = reg_result.get("password")
|
||||||
|
logger.info(f"[Go {task_id}] Registered: {email}")
|
||||||
|
|
||||||
|
# Step 2: 登录获取 Token
|
||||||
|
logger.info(f"[Go {task_id}] Step 2: Logging in...")
|
||||||
|
login_flow = LoginFlow(session, email, password)
|
||||||
|
login_result = await login_flow.run()
|
||||||
|
|
||||||
|
if login_result.get("status") != "success":
|
||||||
|
return {
|
||||||
|
"status": "partial",
|
||||||
|
"failed_stage": "login",
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"error": login_result.get("error", "Login failed"),
|
||||||
|
}
|
||||||
|
|
||||||
|
session.access_token = login_result.get("access_token")
|
||||||
|
session.session_token = login_result.get("session_token")
|
||||||
|
logger.info(f"[Go {task_id}] Logged in, token obtained")
|
||||||
|
|
||||||
|
# Step 3: 获取支付链接
|
||||||
|
logger.info(f"[Go {task_id}] Step 3: Creating checkout session for {plan_name}...")
|
||||||
|
checkout = CheckoutFlow(session, plan_name=plan_name)
|
||||||
|
checkout_result = await checkout.create_checkout_session()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"email": email,
|
||||||
|
"password": password,
|
||||||
|
"access_token": login_result.get("access_token"),
|
||||||
|
"plan_name": plan_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkout_result.get("status") == "success":
|
||||||
|
result["checkout_session_id"] = checkout_result.get("checkout_session_id")
|
||||||
|
result["checkout_url"] = checkout_result.get("url", "")
|
||||||
|
result["client_secret"] = checkout_result.get("client_secret")
|
||||||
|
logger.info(f"[Go {task_id}] Checkout session created")
|
||||||
|
|
||||||
|
# 保存结果
|
||||||
|
from main import save_account, save_checkout_result
|
||||||
|
|
||||||
|
await save_account(result, config.accounts_output_file)
|
||||||
|
checkout_result["email"] = email
|
||||||
|
await save_checkout_result(checkout_result)
|
||||||
|
else:
|
||||||
|
# Checkout 失败但账号创建成功
|
||||||
|
result["status"] = "partial"
|
||||||
|
result["checkout_error"] = checkout_result.get("error")
|
||||||
|
logger.warning(f"[Go {task_id}] Checkout failed: {checkout_result.get('error')}")
|
||||||
|
|
||||||
|
from main import save_account
|
||||||
|
|
||||||
|
await save_account(result, config.accounts_output_file)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"[Go {task_id}] Unexpected error")
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"failed_stage": "exception",
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if session:
|
||||||
|
try:
|
||||||
|
session.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[Go {task_id}] Error closing session: {e}")
|
||||||
96
bot/handlers/settings.py
Normal file
96
bot/handlers/settings.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
"""
|
||||||
|
/set 命令处理器 - 用户设置
|
||||||
|
"""
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from bot.middlewares.auth import require_auth
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_WORKERS = 2
|
||||||
|
MIN_WORKERS = 1
|
||||||
|
MAX_WORKERS = 5
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_settings(context: ContextTypes.DEFAULT_TYPE, user_id: int) -> dict:
|
||||||
|
"""获取用户设置"""
|
||||||
|
if "user_settings" not in context.bot_data:
|
||||||
|
context.bot_data["user_settings"] = {}
|
||||||
|
|
||||||
|
if user_id not in context.bot_data["user_settings"]:
|
||||||
|
context.bot_data["user_settings"][user_id] = {
|
||||||
|
"workers": DEFAULT_WORKERS,
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.bot_data["user_settings"][user_id]
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_setting(
|
||||||
|
context: ContextTypes.DEFAULT_TYPE, user_id: int, key: str, value: any
|
||||||
|
) -> None:
|
||||||
|
"""设置用户配置"""
|
||||||
|
settings = get_user_settings(context, user_id)
|
||||||
|
settings[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
@require_auth
|
||||||
|
async def set_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""
|
||||||
|
处理 /set 命令 - 用户设置
|
||||||
|
|
||||||
|
用法:
|
||||||
|
/set - 查看当前设置
|
||||||
|
/set workers <数量> - 设置并发数 (1-5)
|
||||||
|
"""
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
settings = get_user_settings(context, user_id)
|
||||||
|
|
||||||
|
# 无参数:显示当前设置
|
||||||
|
if not context.args:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⚙️ **当前设置**\n\n"
|
||||||
|
f"👷 并发数: {settings['workers']}\n\n"
|
||||||
|
f"使用 `/set workers <数量>` 修改并发数 ({MIN_WORKERS}-{MAX_WORKERS})",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 解析设置项
|
||||||
|
setting_name = context.args[0].lower()
|
||||||
|
|
||||||
|
if setting_name == "workers":
|
||||||
|
if len(context.args) < 2:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"❌ 请指定并发数\n\n"
|
||||||
|
f"用法: `/set workers <数量>`\n"
|
||||||
|
f"范围: {MIN_WORKERS}-{MAX_WORKERS}",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
workers = int(context.args[1])
|
||||||
|
if workers < MIN_WORKERS:
|
||||||
|
workers = MIN_WORKERS
|
||||||
|
elif workers > MAX_WORKERS:
|
||||||
|
workers = MAX_WORKERS
|
||||||
|
|
||||||
|
set_user_setting(context, user_id, "workers", workers)
|
||||||
|
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"✅ 并发数已设置为 **{workers}**",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"❌ 无效的数值: `{context.args[1]}`",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"❌ 未知设置项: `{setting_name}`\n\n"
|
||||||
|
f"可用设置:\n"
|
||||||
|
f"• `workers` - 并发数 ({MIN_WORKERS}-{MAX_WORKERS})",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
57
bot/handlers/start.py
Normal file
57
bot/handlers/start.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
/start 和 /help 命令处理器
|
||||||
|
"""
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from bot.middlewares.auth import require_auth
|
||||||
|
|
||||||
|
|
||||||
|
@require_auth
|
||||||
|
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""
|
||||||
|
处理 /start 命令 - 欢迎信息
|
||||||
|
"""
|
||||||
|
user = update.effective_user
|
||||||
|
welcome_text = f"""
|
||||||
|
👋 你好, {user.first_name}!
|
||||||
|
|
||||||
|
🔗 **支付链接生成器**
|
||||||
|
|
||||||
|
📋 命令:
|
||||||
|
• `/go [数量] [plan]` - 生成支付链接
|
||||||
|
• `/status [task_id]` - 查看任务状态
|
||||||
|
• `/set workers <数量>` - 设置并发数
|
||||||
|
|
||||||
|
💡 使用 /help 查看详细帮助
|
||||||
|
"""
|
||||||
|
await update.message.reply_text(welcome_text, parse_mode="Markdown")
|
||||||
|
|
||||||
|
|
||||||
|
@require_auth
|
||||||
|
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""
|
||||||
|
处理 /help 命令 - 详细帮助信息
|
||||||
|
"""
|
||||||
|
help_text = """
|
||||||
|
📖 **命令说明**
|
||||||
|
|
||||||
|
**生成支付链接**
|
||||||
|
`/go` - 生成 1 个 Plus 支付链接
|
||||||
|
`/go 5` - 生成 5 个 Plus 支付链接
|
||||||
|
`/go 3 pro` - 生成 3 个 Pro 支付链接
|
||||||
|
|
||||||
|
可选 plan: `plus` (默认) 或 `pro`
|
||||||
|
|
||||||
|
**查看任务状态**
|
||||||
|
`/status` - 查看所有任务
|
||||||
|
`/status <task_id>` - 查看指定任务
|
||||||
|
|
||||||
|
**设置**
|
||||||
|
`/set` - 查看当前设置
|
||||||
|
`/set workers 3` - 设置并发数 (1-5)
|
||||||
|
|
||||||
|
📎 结果将以 JSON 文件形式发送
|
||||||
|
"""
|
||||||
|
await update.message.reply_text(help_text, parse_mode="Markdown")
|
||||||
112
bot/handlers/status.py
Normal file
112
bot/handlers/status.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"""
|
||||||
|
/status 命令处理器 - 查看任务状态
|
||||||
|
"""
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
from bot.middlewares.auth import require_auth
|
||||||
|
from bot.services.task_manager import TaskStatus
|
||||||
|
|
||||||
|
|
||||||
|
STATUS_EMOJI = {
|
||||||
|
TaskStatus.PENDING: "⏳",
|
||||||
|
TaskStatus.RUNNING: "🔄",
|
||||||
|
TaskStatus.COMPLETED: "✅",
|
||||||
|
TaskStatus.FAILED: "❌",
|
||||||
|
TaskStatus.PARTIAL: "⚠️",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@require_auth
|
||||||
|
async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||||
|
"""
|
||||||
|
处理 /status 命令 - 查看任务状态
|
||||||
|
|
||||||
|
用法:
|
||||||
|
/status - 查看所有进行中的任务
|
||||||
|
/status <task_id> - 查看指定任务详情
|
||||||
|
"""
|
||||||
|
task_manager = context.bot_data["task_manager"]
|
||||||
|
user_id = update.effective_user.id
|
||||||
|
|
||||||
|
# 检查是否指定了任务 ID
|
||||||
|
if context.args:
|
||||||
|
task_id = context.args[0]
|
||||||
|
task = task_manager.get_task(task_id)
|
||||||
|
|
||||||
|
if not task:
|
||||||
|
await update.message.reply_text(f"❌ 未找到任务: `{task_id}`", parse_mode="Markdown")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 检查是否是自己的任务(或管理员)
|
||||||
|
admin_users = context.bot_data.get("admin_users", set())
|
||||||
|
if task.user_id != user_id and user_id not in admin_users:
|
||||||
|
await update.message.reply_text("❌ 你没有权限查看此任务")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 显示任务详情
|
||||||
|
emoji = STATUS_EMOJI.get(task.status, "❓")
|
||||||
|
progress_pct = (task.completed / task.total * 100) if task.total > 0 else 0
|
||||||
|
|
||||||
|
text = (
|
||||||
|
f"{emoji} **任务详情**\n\n"
|
||||||
|
f"🆔 ID: `{task.task_id}`\n"
|
||||||
|
f"📋 类型: {task.task_type}\n"
|
||||||
|
f"📝 描述: {task.description}\n"
|
||||||
|
f"📊 状态: {task.status.value}\n"
|
||||||
|
f"📈 进度: {task.completed}/{task.total} ({progress_pct:.0f}%)\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if task.current_item:
|
||||||
|
text += f"🔄 当前: {task.current_item}\n"
|
||||||
|
|
||||||
|
if task.result:
|
||||||
|
text += f"\n📦 结果:\n```\n{task.result}\n```"
|
||||||
|
|
||||||
|
await update.message.reply_text(text, parse_mode="Markdown")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 获取用户的所有任务
|
||||||
|
admin_users = context.bot_data.get("admin_users", set())
|
||||||
|
is_admin = user_id in admin_users
|
||||||
|
|
||||||
|
if is_admin:
|
||||||
|
# 管理员可以看所有任务
|
||||||
|
tasks = task_manager.get_all_tasks()
|
||||||
|
else:
|
||||||
|
# 普通用户只能看自己的
|
||||||
|
tasks = task_manager.get_user_tasks(user_id)
|
||||||
|
|
||||||
|
if not tasks:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"📭 没有找到任务记录\n\n"
|
||||||
|
"使用 `/register` 开始注册账号",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 构建任务列表
|
||||||
|
text = "📋 **任务列表**\n\n"
|
||||||
|
|
||||||
|
# 先显示进行中的任务
|
||||||
|
running_tasks = [t for t in tasks if t.status in [TaskStatus.PENDING, TaskStatus.RUNNING]]
|
||||||
|
completed_tasks = [t for t in tasks if t.status not in [TaskStatus.PENDING, TaskStatus.RUNNING]]
|
||||||
|
|
||||||
|
if running_tasks:
|
||||||
|
text += "**进行中:**\n"
|
||||||
|
for task in running_tasks[:5]:
|
||||||
|
emoji = STATUS_EMOJI.get(task.status, "❓")
|
||||||
|
progress_pct = (task.completed / task.total * 100) if task.total > 0 else 0
|
||||||
|
text += f"{emoji} `{task.task_id}` - {task.description} ({progress_pct:.0f}%)\n"
|
||||||
|
text += "\n"
|
||||||
|
|
||||||
|
if completed_tasks:
|
||||||
|
text += "**已完成:**\n"
|
||||||
|
for task in completed_tasks[:5]:
|
||||||
|
emoji = STATUS_EMOJI.get(task.status, "❓")
|
||||||
|
text += f"{emoji} `{task.task_id}` - {task.description}\n"
|
||||||
|
|
||||||
|
text += f"\n💡 使用 `/status <task_id>` 查看详情"
|
||||||
|
|
||||||
|
await update.message.reply_text(text, parse_mode="Markdown")
|
||||||
7
bot/middlewares/__init__.py
Normal file
7
bot/middlewares/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Bot 中间件模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from bot.middlewares.auth import require_auth, AuthMiddleware
|
||||||
|
|
||||||
|
__all__ = ["require_auth", "AuthMiddleware"]
|
||||||
111
bot/middlewares/auth.py
Normal file
111
bot/middlewares/auth.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"""
|
||||||
|
用户鉴权中间件
|
||||||
|
|
||||||
|
检查用户是否在白名单中
|
||||||
|
"""
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Callable, Any
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
|
||||||
|
class AuthMiddleware:
|
||||||
|
"""
|
||||||
|
鉴权中间件类
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, allowed_users: set, admin_users: set = None):
|
||||||
|
"""
|
||||||
|
初始化鉴权中间件
|
||||||
|
|
||||||
|
参数:
|
||||||
|
allowed_users: 允许使用的用户 ID 集合
|
||||||
|
admin_users: 管理员用户 ID 集合
|
||||||
|
"""
|
||||||
|
self.allowed_users = allowed_users or set()
|
||||||
|
self.admin_users = admin_users or set()
|
||||||
|
|
||||||
|
def is_allowed(self, user_id: int) -> bool:
|
||||||
|
"""检查用户是否允许使用"""
|
||||||
|
# 如果白名单为空,允许所有人
|
||||||
|
if not self.allowed_users:
|
||||||
|
return True
|
||||||
|
return user_id in self.allowed_users or user_id in self.admin_users
|
||||||
|
|
||||||
|
def is_admin(self, user_id: int) -> bool:
|
||||||
|
"""检查用户是否是管理员"""
|
||||||
|
return user_id in self.admin_users
|
||||||
|
|
||||||
|
|
||||||
|
def require_auth(func: Callable) -> Callable:
|
||||||
|
"""
|
||||||
|
鉴权装饰器
|
||||||
|
|
||||||
|
用于命令处理函数,检查用户是否在白名单中
|
||||||
|
|
||||||
|
用法:
|
||||||
|
@require_auth
|
||||||
|
async def my_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs) -> Any:
|
||||||
|
user = update.effective_user
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = user.id
|
||||||
|
allowed_users = context.bot_data.get("allowed_users", set())
|
||||||
|
admin_users = context.bot_data.get("admin_users", set())
|
||||||
|
|
||||||
|
# 如果白名单为空,允许所有人使用
|
||||||
|
if not allowed_users and not admin_users:
|
||||||
|
return await func(update, context, *args, **kwargs)
|
||||||
|
|
||||||
|
# 检查是否在白名单或管理员列表中
|
||||||
|
if user_id in allowed_users or user_id in admin_users:
|
||||||
|
return await func(update, context, *args, **kwargs)
|
||||||
|
|
||||||
|
# 未授权
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"⛔ 你没有权限使用此 Bot\n\n"
|
||||||
|
f"你的用户 ID: `{user_id}`\n\n"
|
||||||
|
f"请联系管理员将你的 ID 添加到白名单",
|
||||||
|
parse_mode="Markdown"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def require_admin(func: Callable) -> Callable:
|
||||||
|
"""
|
||||||
|
管理员权限装饰器
|
||||||
|
|
||||||
|
用于需要管理员权限的命令
|
||||||
|
|
||||||
|
用法:
|
||||||
|
@require_admin
|
||||||
|
async def admin_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE, *args, **kwargs) -> Any:
|
||||||
|
user = update.effective_user
|
||||||
|
if not user:
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = user.id
|
||||||
|
admin_users = context.bot_data.get("admin_users", set())
|
||||||
|
|
||||||
|
if user_id in admin_users:
|
||||||
|
return await func(update, context, *args, **kwargs)
|
||||||
|
|
||||||
|
await update.message.reply_text("⛔ 此命令需要管理员权限")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return wrapper
|
||||||
7
bot/services/__init__.py
Normal file
7
bot/services/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Bot 服务模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from bot.services.task_manager import TaskManager, Task, TaskStatus
|
||||||
|
|
||||||
|
__all__ = ["TaskManager", "Task", "TaskStatus"]
|
||||||
101
bot/services/file_sender.py
Normal file
101
bot/services/file_sender.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""
|
||||||
|
JSON 文件生成与发送服务
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import ContextTypes
|
||||||
|
|
||||||
|
|
||||||
|
async def send_results_as_json(
|
||||||
|
update: Update,
|
||||||
|
context: ContextTypes.DEFAULT_TYPE,
|
||||||
|
task_id: str,
|
||||||
|
plan: str,
|
||||||
|
results: list[dict[str, Any]],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
将结果生成为 JSON 文件并发送给用户
|
||||||
|
|
||||||
|
参数:
|
||||||
|
update: Telegram Update 对象
|
||||||
|
context: Telegram Context 对象
|
||||||
|
task_id: 任务 ID
|
||||||
|
plan: 订阅计划名称
|
||||||
|
results: 结果列表
|
||||||
|
"""
|
||||||
|
# 统计
|
||||||
|
success_count = sum(1 for r in results if r.get("status") == "success")
|
||||||
|
failed_count = len(results) - success_count
|
||||||
|
|
||||||
|
# 构建 JSON 数据
|
||||||
|
now = datetime.now()
|
||||||
|
data = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"plan": plan,
|
||||||
|
"created_at": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"email": r.get("email", ""),
|
||||||
|
"password": r.get("password", ""),
|
||||||
|
"url": r.get("checkout_url", ""),
|
||||||
|
"checkout_session_id": r.get("checkout_session_id", ""),
|
||||||
|
"status": r.get("status", "failed"),
|
||||||
|
}
|
||||||
|
for r in results
|
||||||
|
],
|
||||||
|
"summary": {
|
||||||
|
"total": len(results),
|
||||||
|
"success": success_count,
|
||||||
|
"failed": failed_count,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生成文件名
|
||||||
|
timestamp = now.strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = f"checkout_{timestamp}.json"
|
||||||
|
|
||||||
|
# 写入临时文件并发送
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w",
|
||||||
|
suffix=".json",
|
||||||
|
prefix="checkout_",
|
||||||
|
delete=False,
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
temp_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 发送消息(不含敏感信息)
|
||||||
|
message_text = (
|
||||||
|
f"✅ **任务完成**\n\n"
|
||||||
|
f"📦 Plan: {plan}\n"
|
||||||
|
f"✅ 成功: {success_count}\n"
|
||||||
|
f"❌ 失败: {failed_count}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
await context.bot.send_message(
|
||||||
|
chat_id=update.effective_chat.id,
|
||||||
|
text=message_text,
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 发送 JSON 文件
|
||||||
|
with open(temp_path, "rb") as f:
|
||||||
|
await context.bot.send_document(
|
||||||
|
chat_id=update.effective_chat.id,
|
||||||
|
document=f,
|
||||||
|
filename=filename,
|
||||||
|
caption=f"📎 任务 `{task_id}` 结果",
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if os.path.exists(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
228
bot/services/task_manager.py
Normal file
228
bot/services/task_manager.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
"""
|
||||||
|
后台任务管理器
|
||||||
|
|
||||||
|
管理异步执行的注册、登录等任务的状态和进度
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatus(Enum):
|
||||||
|
"""任务状态枚举"""
|
||||||
|
PENDING = "pending" # 等待执行
|
||||||
|
RUNNING = "running" # 执行中
|
||||||
|
COMPLETED = "completed" # 已完成
|
||||||
|
FAILED = "failed" # 失败
|
||||||
|
PARTIAL = "partial" # 部分成功
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Task:
|
||||||
|
"""任务数据类"""
|
||||||
|
task_id: str
|
||||||
|
user_id: int
|
||||||
|
task_type: str # register, login, checkout
|
||||||
|
status: TaskStatus
|
||||||
|
description: str
|
||||||
|
total: int = 0 # 总任务数
|
||||||
|
completed: int = 0 # 已完成数
|
||||||
|
current_item: str = "" # 当前处理项
|
||||||
|
result: Optional[Any] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
created_at: datetime = field(default_factory=datetime.now)
|
||||||
|
updated_at: datetime = field(default_factory=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskManager:
|
||||||
|
"""
|
||||||
|
任务管理器
|
||||||
|
|
||||||
|
管理后台任务的创建、状态更新和查询
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, max_history: int = 100):
|
||||||
|
"""
|
||||||
|
初始化任务管理器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
max_history: 保留的历史任务数量
|
||||||
|
"""
|
||||||
|
self._tasks: Dict[str, Task] = {}
|
||||||
|
self._max_history = max_history
|
||||||
|
|
||||||
|
def create_task(
|
||||||
|
self,
|
||||||
|
user_id: int,
|
||||||
|
task_type: str,
|
||||||
|
total: int = 1,
|
||||||
|
description: str = ""
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
创建新任务
|
||||||
|
|
||||||
|
参数:
|
||||||
|
user_id: 用户 ID
|
||||||
|
task_type: 任务类型 (register, login, checkout)
|
||||||
|
total: 总任务数
|
||||||
|
description: 任务描述
|
||||||
|
|
||||||
|
返回:
|
||||||
|
任务 ID
|
||||||
|
"""
|
||||||
|
task_id = self._generate_task_id()
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
task_id=task_id,
|
||||||
|
user_id=user_id,
|
||||||
|
task_type=task_type,
|
||||||
|
status=TaskStatus.RUNNING,
|
||||||
|
description=description or f"{task_type} task",
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._tasks[task_id] = task
|
||||||
|
self._cleanup_old_tasks()
|
||||||
|
|
||||||
|
return task_id
|
||||||
|
|
||||||
|
def update_progress(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
completed: Optional[int] = None,
|
||||||
|
current_item: Optional[str] = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
更新任务进度
|
||||||
|
|
||||||
|
参数:
|
||||||
|
task_id: 任务 ID
|
||||||
|
completed: 已完成数量
|
||||||
|
current_item: 当前处理项描述
|
||||||
|
|
||||||
|
返回:
|
||||||
|
是否更新成功
|
||||||
|
"""
|
||||||
|
task = self._tasks.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if completed is not None:
|
||||||
|
task.completed = completed
|
||||||
|
if current_item is not None:
|
||||||
|
task.current_item = current_item
|
||||||
|
|
||||||
|
task.updated_at = datetime.now()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def complete_task(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
status: TaskStatus = TaskStatus.COMPLETED,
|
||||||
|
result: Any = None,
|
||||||
|
error: str = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
完成任务
|
||||||
|
|
||||||
|
参数:
|
||||||
|
task_id: 任务 ID
|
||||||
|
status: 最终状态
|
||||||
|
result: 任务结果
|
||||||
|
error: 错误信息(如果失败)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
是否更新成功
|
||||||
|
"""
|
||||||
|
task = self._tasks.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return False
|
||||||
|
|
||||||
|
task.status = status
|
||||||
|
task.result = result
|
||||||
|
task.error = error
|
||||||
|
task.completed = task.total
|
||||||
|
task.current_item = ""
|
||||||
|
task.updated_at = datetime.now()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_task(self, task_id: str) -> Optional[Task]:
|
||||||
|
"""
|
||||||
|
获取任务
|
||||||
|
|
||||||
|
参数:
|
||||||
|
task_id: 任务 ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
Task 对象,如果不存在返回 None
|
||||||
|
"""
|
||||||
|
return self._tasks.get(task_id)
|
||||||
|
|
||||||
|
def get_user_tasks(self, user_id: int, limit: int = 10) -> List[Task]:
|
||||||
|
"""
|
||||||
|
获取用户的任务列表
|
||||||
|
|
||||||
|
参数:
|
||||||
|
user_id: 用户 ID
|
||||||
|
limit: 返回数量限制
|
||||||
|
|
||||||
|
返回:
|
||||||
|
任务列表(按创建时间倒序)
|
||||||
|
"""
|
||||||
|
user_tasks = [
|
||||||
|
task for task in self._tasks.values()
|
||||||
|
if task.user_id == user_id
|
||||||
|
]
|
||||||
|
user_tasks.sort(key=lambda t: t.created_at, reverse=True)
|
||||||
|
return user_tasks[:limit]
|
||||||
|
|
||||||
|
def get_all_tasks(self, limit: int = 20) -> List[Task]:
|
||||||
|
"""
|
||||||
|
获取所有任务(管理员用)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
limit: 返回数量限制
|
||||||
|
|
||||||
|
返回:
|
||||||
|
任务列表(按创建时间倒序)
|
||||||
|
"""
|
||||||
|
all_tasks = list(self._tasks.values())
|
||||||
|
all_tasks.sort(key=lambda t: t.created_at, reverse=True)
|
||||||
|
return all_tasks[:limit]
|
||||||
|
|
||||||
|
def get_running_tasks(self) -> List[Task]:
|
||||||
|
"""
|
||||||
|
获取正在运行的任务
|
||||||
|
|
||||||
|
返回:
|
||||||
|
运行中的任务列表
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
task for task in self._tasks.values()
|
||||||
|
if task.status in [TaskStatus.PENDING, TaskStatus.RUNNING]
|
||||||
|
]
|
||||||
|
|
||||||
|
def _generate_task_id(self) -> str:
|
||||||
|
"""生成唯一任务 ID"""
|
||||||
|
return uuid.uuid4().hex[:8]
|
||||||
|
|
||||||
|
def _cleanup_old_tasks(self):
|
||||||
|
"""清理旧任务,保持历史记录在限制内"""
|
||||||
|
if len(self._tasks) <= self._max_history:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 获取已完成的任务,按时间排序
|
||||||
|
completed_tasks = [
|
||||||
|
(task_id, task) for task_id, task in self._tasks.items()
|
||||||
|
if task.status not in [TaskStatus.PENDING, TaskStatus.RUNNING]
|
||||||
|
]
|
||||||
|
completed_tasks.sort(key=lambda x: x[1].created_at)
|
||||||
|
|
||||||
|
# 删除最旧的任务
|
||||||
|
to_remove = len(self._tasks) - self._max_history
|
||||||
|
for task_id, _ in completed_tasks[:to_remove]:
|
||||||
|
del self._tasks[task_id]
|
||||||
104
bot_main.py
Normal file
104
bot_main.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Telegram Bot 启动入口
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
python bot_main.py
|
||||||
|
|
||||||
|
配置 (.env):
|
||||||
|
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||||
|
TELEGRAM_ALLOWED_USERS=123456789,987654321
|
||||||
|
TELEGRAM_ADMIN_USERS=123456789
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 确保项目根目录在 Python 路径中
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from config import load_config
|
||||||
|
from utils.logger import logger, setup_logger
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
print("=" * 60)
|
||||||
|
print(" OpenAI 账号自动注册系统 - Telegram Bot")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
config = load_config()
|
||||||
|
setup_logger(config.log_level)
|
||||||
|
|
||||||
|
# 检查配置
|
||||||
|
if not config.telegram_bot_enabled:
|
||||||
|
logger.error("Telegram Bot is disabled in config")
|
||||||
|
print("❌ Telegram Bot 已禁用,请设置 TELEGRAM_BOT_ENABLED=true")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not config.telegram_bot_token:
|
||||||
|
logger.error("TELEGRAM_BOT_TOKEN is not configured")
|
||||||
|
print("❌ 请在 .env 文件中配置 TELEGRAM_BOT_TOKEN")
|
||||||
|
print()
|
||||||
|
print("步骤:")
|
||||||
|
print("1. 在 Telegram 中找到 @BotFather")
|
||||||
|
print("2. 发送 /newbot 创建新 Bot")
|
||||||
|
print("3. 将获得的 Token 填入 .env 文件:")
|
||||||
|
print(" TELEGRAM_BOT_TOKEN=your_token_here")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 打印配置信息
|
||||||
|
allowed_count = len([x for x in config.telegram_allowed_users.split(",") if x.strip()])
|
||||||
|
admin_count = len([x for x in config.telegram_admin_users.split(",") if x.strip()])
|
||||||
|
|
||||||
|
logger.info(f"Bot Token: {config.telegram_bot_token[:10]}...")
|
||||||
|
logger.info(f"Allowed users: {allowed_count} configured")
|
||||||
|
logger.info(f"Admin users: {admin_count} configured")
|
||||||
|
|
||||||
|
if not config.telegram_allowed_users and not config.telegram_admin_users:
|
||||||
|
logger.warning("⚠️ No user whitelist configured - Bot will be accessible to everyone!")
|
||||||
|
print()
|
||||||
|
print("⚠️ 警告: 未配置用户白名单,所有人都可以使用此 Bot")
|
||||||
|
print(" 建议在 .env 中添加:")
|
||||||
|
print(" TELEGRAM_ALLOWED_USERS=your_user_id")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
Path("output/logs").mkdir(parents=True, exist_ok=True)
|
||||||
|
Path("output/tokens").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# 创建并运行 Bot
|
||||||
|
try:
|
||||||
|
from bot.app import create_application
|
||||||
|
|
||||||
|
logger.info("Creating Telegram Bot application...")
|
||||||
|
application = create_application(config)
|
||||||
|
|
||||||
|
logger.info("Starting Bot in polling mode...")
|
||||||
|
print()
|
||||||
|
print("🤖 Bot 已启动! 在 Telegram 中发送 /start 开始使用")
|
||||||
|
print(" 按 Ctrl+C 停止")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 运行 Bot
|
||||||
|
application.run_polling(
|
||||||
|
allowed_updates=["message"],
|
||||||
|
drop_pending_updates=True # 忽略 Bot 离线期间的消息
|
||||||
|
)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print()
|
||||||
|
logger.info("Bot stopped by user")
|
||||||
|
print("👋 Bot 已停止")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Failed to start Bot")
|
||||||
|
print(f"❌ 启动失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
18
config.py
18
config.py
@@ -287,6 +287,24 @@ class AppConfig(BaseSettings):
|
|||||||
description="HTTP 请求超时时间(秒)"
|
description="HTTP 请求超时时间(秒)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ========== Telegram Bot 配置 ==========
|
||||||
|
telegram_bot_token: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Telegram Bot Token (从 @BotFather 获取)"
|
||||||
|
)
|
||||||
|
telegram_allowed_users: str = Field(
|
||||||
|
default="",
|
||||||
|
description="允许使用 Bot 的用户 ID(逗号分隔)"
|
||||||
|
)
|
||||||
|
telegram_admin_users: str = Field(
|
||||||
|
default="",
|
||||||
|
description="管理员用户 ID(逗号分隔,拥有更高权限)"
|
||||||
|
)
|
||||||
|
telegram_bot_enabled: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="是否启用 Telegram Bot"
|
||||||
|
)
|
||||||
|
|
||||||
@field_validator("proxy_pool")
|
@field_validator("proxy_pool")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_proxy_pool(cls, v: str) -> str:
|
def validate_proxy_pool(cls, v: str) -> str:
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ dependencies = [
|
|||||||
"loguru>=0.7.2", # 日志系统
|
"loguru>=0.7.2", # 日志系统
|
||||||
"python-dotenv>=1.0.0", # 环境变量加载
|
"python-dotenv>=1.0.0", # 环境变量加载
|
||||||
"httpx>=0.25.0", # 异步 HTTP 客户端(可选,用于外部 API)
|
"httpx>=0.25.0", # 异步 HTTP 客户端(可选,用于外部 API)
|
||||||
|
"python-telegram-bot[job-queue]>=20.7", # Telegram Bot
|
||||||
]
|
]
|
||||||
|
|||||||
57
uv.lock
generated
57
uv.lock
generated
@@ -1,6 +1,10 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.14'",
|
||||||
|
"python_full_version < '3.14'",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
@@ -24,6 +28,18 @@ 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" },
|
{ 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 = "apscheduler"
|
||||||
|
version = "3.11.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzlocal" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.1.4"
|
version = "2026.1.4"
|
||||||
@@ -133,6 +149,7 @@ dependencies = [
|
|||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "python-telegram-bot", extra = ["job-queue"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -143,6 +160,7 @@ requires-dist = [
|
|||||||
{ name = "pydantic", specifier = ">=2.5.0" },
|
{ name = "pydantic", specifier = ">=2.5.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.1.0" },
|
{ name = "pydantic-settings", specifier = ">=2.1.0" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||||
|
{ name = "python-telegram-bot", extras = ["job-queue"], specifier = ">=20.7" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -322,6 +340,24 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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.optional-dependencies]
|
||||||
|
job-queue = [
|
||||||
|
{ name = "apscheduler" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.15.0"
|
||||||
@@ -343,6 +379,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzlocal"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "win32-setctime"
|
name = "win32-setctime"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user