Files
tg-spam-guard/main.py
T

159 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""TG 群管机器人 v2 — FastAPI + Tortoise ORM + python-telegram-bot"""
import os
import time
import asyncio
import logging
from contextlib import asynccontextmanager
# 代理(必须在其他 import 之前)
PROXY_URL = os.getenv("PROXY_URL", "http://192.168.123.10:11055")
os.environ["HTTPS_PROXY"] = PROXY_URL
os.environ["HTTP_PROXY"] = PROXY_URL
os.environ["NO_PROXY"] = os.getenv("NO_PROXY", "127.0.0.1,localhost")
import uvicorn
from fastapi import FastAPI
from telegram import BotCommand, BotCommandScopeChat
from telegram.ext import Application
import config
from db import init_db, close_db
from handlers import register_all
# ============ 日志 ============
log_fmt = "%(asctime)s [%(levelname)s] %(message)s"
log_datefmt = "%Y-%m-%d %H:%M:%S"
logging.basicConfig(level=logging.INFO, format=log_fmt, datefmt=log_datefmt)
logger = logging.getLogger("spam_guard")
# 阻止 httpx 刷屏
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
# ============ Telegram Bot ============
tg_app: Application = None
_last_polling_error_log = 0.0
def polling_error_callback(exc):
"""压缩 Telegram polling 网络异常日志,避免代理偶发断连刷 traceback。"""
global _last_polling_error_log
now = time.time()
# polling 会自动重试;同类网络波动 60 秒内只提示一次。
if now - _last_polling_error_log < 60:
return
_last_polling_error_log = now
logger.warning(f"⚠️ TG polling 网络波动,已自动重试: {type(exc).__name__}: {exc}")
async def start_tg_bot():
global tg_app
from telegram.request import HTTPXRequest
proxy_request = HTTPXRequest(proxy=PROXY_URL, connect_timeout=20.0, read_timeout=20.0, write_timeout=20.0, pool_timeout=5.0)
tg_app = (
Application.builder()
.token(config.TG_BOT_TOKEN)
.request(proxy_request)
.build()
)
register_all(tg_app)
await tg_app.initialize()
await tg_app.start()
await tg_app.updater.start_polling(drop_pending_updates=False, error_callback=polling_error_callback)
# 菜单
commands = [
BotCommand("ask", "🤖 向知识库提问"),
BotCommand("stats", "📊 今日统计"),
BotCommand("checkin", "✅ 每日签到"),
BotCommand("trivia", "🎮 知识答题"),
BotCommand("bind", "🔗 绑定账号"),
BotCommand("help", "📖 帮助"),
]
await tg_app.bot.delete_my_commands()
await tg_app.bot.set_my_commands(commands, scope=BotCommandScopeChat(chat_id=config.TG_CHAT_ID))
logger.info(f"✅ TG Bot 已启动,菜单命令已设置 (群组: {config.TG_CHAT_ID})")
async def stop_tg_bot():
global tg_app
if tg_app:
try:
if tg_app.updater and tg_app.updater.running:
await tg_app.updater.stop()
if tg_app.running:
await tg_app.stop()
await tg_app.shutdown()
logger.info("🛑 TG Bot 已停止")
except Exception as e:
logger.warning(f"⚠️ TG Bot 停止/清理异常: {type(e).__name__}: {e}")
finally:
tg_app = None
# ============ FastAPI ============
from web.api import app as web_app
async def start_tg_bot_safe():
"""启动 TG Bot;初始化阶段网络失败时压缩日志并自动重试。"""
delay = 10
while True:
try:
await start_tg_bot()
return
except asyncio.CancelledError:
raise
except Exception as e:
logger.error(f"❌ TG Bot 启动失败,将在 {delay}s 后重试: {type(e).__name__}: {e}")
try:
await stop_tg_bot()
except Exception:
pass
await asyncio.sleep(delay)
delay = min(120, delay * 2)
@asynccontextmanager
async def lifespan(app):
# 初始化数据库
await init_db()
# 启动 TG Bot
task = asyncio.create_task(start_tg_bot_safe())
def log_task_result(t: asyncio.Task):
if t.cancelled():
return
exc = t.exception()
if exc:
logger.error("❌ TG Bot 任务异常结束", exc_info=exc)
task.add_done_callback(log_task_result)
logger.info("🚀 服务启动完成")
try:
yield
finally:
await stop_tg_bot()
await close_db()
logger.info("🛑 服务已关闭")
web_app.router.lifespan_context = lifespan
def run():
uvicorn.run(web_app, host="0.0.0.0", port=config.WEB_PORT, log_level="info", access_log=False)
if __name__ == "__main__":
run()