添加后台 Telegram 审计面板
This commit is contained in:
@@ -22,6 +22,7 @@ from web.services.ops_api import (
|
||||
search_ops_users,
|
||||
update_ops_config,
|
||||
get_ops_training_accuracy,
|
||||
get_ops_telegram_audit,
|
||||
)
|
||||
|
||||
router = APIRouter(tags=["ops"])
|
||||
@@ -167,3 +168,9 @@ async def ops_health_check(request: Request):
|
||||
async def ops_training_accuracy(request: Request):
|
||||
return get_ops_training_accuracy(request)
|
||||
|
||||
|
||||
@router.get("/api/ops/telegram/members-audit")
|
||||
async def ops_telegram_audit(request: Request):
|
||||
return get_ops_telegram_audit(request)
|
||||
|
||||
|
||||
|
||||
@@ -836,3 +836,175 @@ def get_ops_training_accuracy(request: Request) -> Dict[str, Any]:
|
||||
|
||||
return {"accuracy": accuracy_data}
|
||||
|
||||
|
||||
def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
||||
_require_ops(request)
|
||||
import concurrent.futures
|
||||
import os
|
||||
import sqlite3
|
||||
import requests
|
||||
from src.database.db_manager import DBManager
|
||||
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||
|
||||
db = DBManager()
|
||||
|
||||
# 1. Fetch all distinct telegram users from database
|
||||
with db._get_connection() as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
users_rows = conn.execute("SELECT telegram_id, username FROM users").fetchall()
|
||||
bindings_rows = conn.execute("SELECT telegram_id, supabase_user_id, supabase_email FROM supabase_bindings").fetchall()
|
||||
|
||||
user_info = {}
|
||||
for r in users_rows:
|
||||
tid = int(r["telegram_id"])
|
||||
user_info[tid] = {
|
||||
"telegram_id": tid,
|
||||
"username": r["username"] or f"ID: {tid}",
|
||||
"supabase_user_id": None,
|
||||
"supabase_email": None,
|
||||
"is_bound": False
|
||||
}
|
||||
|
||||
for r in bindings_rows:
|
||||
tid = int(r["telegram_id"])
|
||||
if tid not in user_info:
|
||||
user_info[tid] = {
|
||||
"telegram_id": tid,
|
||||
"username": f"ID: {tid}",
|
||||
"supabase_user_id": r["supabase_user_id"],
|
||||
"supabase_email": r["supabase_email"],
|
||||
"is_bound": True
|
||||
}
|
||||
else:
|
||||
user_info[tid]["supabase_user_id"] = r["supabase_user_id"]
|
||||
user_info[tid]["supabase_email"] = r["supabase_email"]
|
||||
user_info[tid]["is_bound"] = True
|
||||
|
||||
# 2. Get Telegram Bot settings
|
||||
bot_token = str(os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
|
||||
chat_ids = get_telegram_chat_ids_from_env()
|
||||
|
||||
# Add other group IDs if configured
|
||||
for env_name in ["POLYWEATHER_TELEGRAM_GROUP_ID", "POLYWEATHER_TELEGRAM_TOPICS_GROUP_ID"]:
|
||||
val = str(os.getenv(env_name) or "").strip()
|
||||
if val and val not in chat_ids:
|
||||
chat_ids.append(val)
|
||||
|
||||
if not bot_token or not chat_ids:
|
||||
return {"error": "Telegram Bot Token or Chat IDs not configured", "anomalies": []}
|
||||
|
||||
# 3. Check membership status for all users in parallel
|
||||
results = []
|
||||
|
||||
def check_user_chat(tg_id, chat_id):
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"https://api.telegram.org/bot{bot_token}/getChatMember",
|
||||
params={"chat_id": chat_id, "user_id": tg_id},
|
||||
timeout=5
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
if data.get("ok"):
|
||||
res_status = data["result"].get("status")
|
||||
return chat_id, res_status
|
||||
return chat_id, None
|
||||
except Exception:
|
||||
return chat_id, None
|
||||
|
||||
all_tasks = []
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = {}
|
||||
for tg_id in user_info.keys():
|
||||
for c_id in chat_ids:
|
||||
f = executor.submit(check_user_chat, tg_id, c_id)
|
||||
futures[f] = (tg_id, c_id)
|
||||
|
||||
for f in concurrent.futures.as_completed(futures):
|
||||
tg_id, c_id = futures[f]
|
||||
try:
|
||||
_, status = f.result()
|
||||
if status in {"creator", "administrator", "member"}:
|
||||
results.append((tg_id, c_id, status))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 4. Filter and categorize members
|
||||
anomalies = []
|
||||
valid_members = []
|
||||
|
||||
from web.services.ops_api import legacy_routes
|
||||
active_subs = legacy_routes.SUPABASE_ENTITLEMENT.list_active_subscriptions(limit=5000)
|
||||
active_subs_map = {}
|
||||
for sub in active_subs:
|
||||
uid = str(sub.get("user_id") or "").strip().lower()
|
||||
if uid:
|
||||
active_subs_map[uid] = sub
|
||||
|
||||
for tg_id, chat_id, status in results:
|
||||
info = user_info[tg_id]
|
||||
|
||||
if not info["is_bound"]:
|
||||
anomalies.append({
|
||||
"telegram_id": tg_id,
|
||||
"username": info["username"],
|
||||
"chat_id": chat_id,
|
||||
"status": status,
|
||||
"anomaly_type": "unbound",
|
||||
"reason": "未绑定网页账号",
|
||||
"email": None,
|
||||
"expires_at": None,
|
||||
})
|
||||
else:
|
||||
uid = str(info["supabase_user_id"]).strip().lower()
|
||||
sub = active_subs_map.get(uid)
|
||||
is_paid = False
|
||||
plan_code = ""
|
||||
expires_at = None
|
||||
|
||||
if sub:
|
||||
plan_code = str(sub.get("plan_code") or "").strip().lower()
|
||||
source = str(sub.get("source") or "").strip().lower()
|
||||
is_paid = "trial" not in plan_code and "trial" not in source
|
||||
expires_at = sub.get("expires_at")
|
||||
|
||||
if not sub:
|
||||
anomalies.append({
|
||||
"telegram_id": tg_id,
|
||||
"username": info["username"],
|
||||
"chat_id": chat_id,
|
||||
"status": status,
|
||||
"anomaly_type": "expired",
|
||||
"reason": "没有有效的会员订阅",
|
||||
"email": info["supabase_email"],
|
||||
"expires_at": None,
|
||||
})
|
||||
elif not is_paid:
|
||||
anomalies.append({
|
||||
"telegram_id": tg_id,
|
||||
"username": info["username"],
|
||||
"chat_id": chat_id,
|
||||
"status": status,
|
||||
"anomaly_type": "trial_only",
|
||||
"reason": f"仅拥有试用会员 ({plan_code})",
|
||||
"email": info["supabase_email"],
|
||||
"expires_at": expires_at,
|
||||
})
|
||||
else:
|
||||
valid_members.append({
|
||||
"telegram_id": tg_id,
|
||||
"username": info["username"],
|
||||
"chat_id": chat_id,
|
||||
"status": status,
|
||||
"email": info["supabase_email"],
|
||||
"plan_code": plan_code,
|
||||
"expires_at": expires_at,
|
||||
})
|
||||
|
||||
return {
|
||||
"anomalies": anomalies,
|
||||
"valid_count": len(valid_members),
|
||||
"anomaly_count": len(anomalies),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user