From c9263bce3d8d93fd63f92e08270d0d18909a83e8 Mon Sep 17 00:00:00 2001 From: InfernoXuaI <1391197588@qq.com> Date: Sun, 31 May 2026 12:06:06 +0800 Subject: [PATCH 1/2] 531 --- README.md | 5 +- app.py | 221 +++++++++++++++++++++++++++++++++++++++++++++-- requirements.txt | 1 + 3 files changed, 217 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 23c8122..8f8c44a 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ tg-watchbot 是一个轻量级 Python 服务,把 **Telegram 双向客服机器 - 支持关键词过滤:只转发包含特定关键词的消息,留空则转发全部。 - 支持媒体类型过滤:可选视频、文档、图片、音频。 - 支持 SOCKS5/HTTP 代理,适合国内服务器。 +- 新增 Telegram 二维码登录:设置页填写 `TG_API_ID` / `TG_API_HASH` 后,可扫码生成并保存用户会话。 - 内置下载到服务器、断点续传、并发下载等功能,后续可通过配置开启。 -- 需要在设置页填写 `TG_API_ID`、`TG_API_HASH`、`TG_API_SESSION` 后使用。 +- 仍兼容手动填写 `TG_API_SESSION`。 ### 2026-05-22 更新 @@ -126,7 +127,7 @@ tg-watchbot 是一个轻量级 Python 服务,把 **Telegram 双向客服机器 - 支持关键词过滤、媒体类型选择(视频/文档/图片/音频)、文件大小限制。 - 支持实时转发模式:群消息匹配后直接转发到你的 Telegram(含视频/文档原文),无需下载到服务器。 - 下载完成可自动推送 Telegram 通知给管理员。 -- 需要在设置页填写 `TG_API_ID`、`TG_API_HASH`、`TG_API_SESSION`。 +- 需要在设置页填写 `TG_API_ID`、`TG_API_HASH`,然后扫码登录;也兼容手动填写 `TG_API_SESSION`。 ### Web 管理面板 diff --git a/app.py b/app.py index 0e1cb2c..80d35cc 100644 --- a/app.py +++ b/app.py @@ -10,8 +10,10 @@ from __future__ import annotations import argparse import asyncio +import base64 import hashlib import html +import io import logging import os import re @@ -32,6 +34,7 @@ import os.path as ospath import feedparser import httpx import yaml +import qrcode from apscheduler.schedulers.asyncio import AsyncIOScheduler from bs4 import BeautifulSoup from dotenv import load_dotenv @@ -81,6 +84,7 @@ scheduler_ref: AsyncIOScheduler | None = None user_session_listener_task: asyncio.Task | None = None user_session_client: Any = None channel_media_clients: dict[str, Any] = {} +telegram_qr_logins: dict[str, dict[str, Any]] = {} GROUP_SUMMARY_MAX_CHARS = 800 @@ -225,6 +229,19 @@ def init_db() -> None: meta_value TEXT, updated_at TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS telegram_login_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_name TEXT NOT NULL DEFAULT 'default', + api_id TEXT NOT NULL DEFAULT '', + api_hash TEXT NOT NULL DEFAULT '', + tg_session TEXT NOT NULL DEFAULT '', + phone TEXT DEFAULT '', + username TEXT DEFAULT '', + user_id TEXT DEFAULT '', + status TEXT DEFAULT 'empty', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); CREATE TABLE IF NOT EXISTS discovered_group_chats ( chat_id INTEGER PRIMARY KEY, title TEXT, @@ -281,6 +298,10 @@ def init_db() -> None: "ALTER TABLE channel_media_monitors ADD COLUMN max_concurrent INTEGER DEFAULT 3", "ALTER TABLE channel_media_monitors ADD COLUMN forward_mode INTEGER DEFAULT 0", "ALTER TABLE channel_media_monitors ADD COLUMN forward_to TEXT DEFAULT 'admin'", + "ALTER TABLE telegram_login_sessions ADD COLUMN username TEXT DEFAULT ''", + "ALTER TABLE telegram_login_sessions ADD COLUMN user_id TEXT DEFAULT ''", + "ALTER TABLE telegram_login_sessions ADD COLUMN phone TEXT DEFAULT ''", + "ALTER TABLE telegram_login_sessions ADD COLUMN status TEXT DEFAULT 'empty'", ]: try: conn.execute(sql) @@ -568,6 +589,14 @@ def user_session_config() -> tuple[str, str, str]: api_id = os.getenv("TG_API_ID", "").strip() api_hash = os.getenv("TG_API_HASH", "").strip() session = os.getenv("TG_API_SESSION", "").strip() + with closing(db()) as conn: + row = conn.execute( + "SELECT api_id, api_hash, tg_session FROM telegram_login_sessions WHERE session_name='default' ORDER BY id DESC LIMIT 1" + ).fetchone() + if row: + api_id = str(row["api_id"] or api_id).strip() + api_hash = str(row["api_hash"] or api_hash).strip() + session = str(row["tg_session"] or session).strip() return api_id, api_hash, session @@ -584,6 +613,91 @@ def user_session_ready() -> bool: return True +def telegram_login_status_row() -> dict[str, str]: + with closing(db()) as conn: + row = conn.execute( + "SELECT * FROM telegram_login_sessions WHERE session_name='default' ORDER BY id DESC LIMIT 1" + ).fetchone() + if not row: + return {"status": "empty", "username": "", "phone": "", "user_id": ""} + return {k: str(row[k] or "") for k in row.keys()} + + +def save_telegram_login_session(api_id: str, api_hash: str, tg_session: str, phone: str = "", username: str = "", user_id: str = "") -> None: + ts = now_iso() + with closing(db()) as conn: + conn.execute( + "DELETE FROM telegram_login_sessions WHERE session_name='default'" + ) + conn.execute( + """INSERT INTO telegram_login_sessions(session_name, api_id, api_hash, tg_session, phone, username, user_id, status, created_at, updated_at) + VALUES('default',?,?,?,?,?,?, 'authorized', ?, ?)""", + (api_id, api_hash, tg_session, phone, username, user_id, ts, ts), + ) + conn.commit() + + +def clear_telegram_login_session() -> None: + with closing(db()) as conn: + conn.execute("DELETE FROM telegram_login_sessions WHERE session_name='default'") + conn.commit() + + +async def telegram_login_prepare_qr() -> dict[str, Any]: + if TelegramClient is None or StringSession is None: + return {"ok": False, "error": "telethon not installed"} + api_id = os.getenv("TG_API_ID", "").strip() + api_hash = os.getenv("TG_API_HASH", "").strip() + if not api_id or not api_hash: + return {"ok": False, "error": "missing TG_API_ID / TG_API_HASH"} + try: + api_id_int = int(api_id) + except Exception: + return {"ok": False, "error": "TG_API_ID must be int"} + client = TelegramClient(StringSession(), api_id_int, api_hash) + try: + await client.connect() + qr_login = await client.qr_login() + qr_svg = qrcode.make(qr_login.url) + buffer = io.BytesIO() + qr_svg.save(buffer, format="PNG") + qr_b64 = base64.b64encode(buffer.getvalue()).decode("ascii") + return { + "ok": True, + "url": qr_login.url, + "qr_png": f"data:image/png;base64,{qr_b64}", + "client": client, + "login": qr_login, + } + except Exception as e: + try: + await client.disconnect() + except Exception: + pass + return {"ok": False, "error": str(e)} + + +async def telegram_login_complete(client: Any, login: Any) -> dict[str, Any]: + await login.wait() + session_str = client.session.save() if hasattr(client.session, "save") else "" + me = await client.get_me() + save_telegram_login_session( + os.getenv("TG_API_ID", "").strip(), + os.getenv("TG_API_HASH", "").strip(), + session_str, + phone=str(getattr(me, "phone", "") or ""), + username=str(getattr(me, "username", "") or ""), + user_id=str(getattr(me, "id", "") or ""), + ) + await client.disconnect() + return { + "ok": True, + "username": str(getattr(me, "username", "") or ""), + "phone": str(getattr(me, "phone", "") or ""), + "user_id": str(getattr(me, "id", "") or ""), + } + + # ---- Channel Media Download (Telethon user session) ---- def get_or_create_channel_media_client(client_type: str = "channel_media", proxy: str = "") -> Any: @@ -2824,6 +2938,9 @@ th{{color:var(--ink);font-size:12px;background:var(--yellow);text-transform:uppe tr:nth-child(even) td{{background:#fafafa}} .badge{{padding:4px 8px;border:3px solid var(--ink);border-radius:999px;background:var(--blue);color:white;font-size:12px;font-weight:900;text-transform:uppercase}} .msg{{padding:11px 12px;border:3px solid var(--ink);background:var(--yellow);color:var(--ink);margin:10px 0;font-weight:900;box-shadow:4px 4px 0 var(--ink)}} +.step{{border:3px solid var(--ink);background:#fff;padding:14px;margin:14px 0;box-shadow:4px 4px 0 var(--ink)}} +.step-title{{display:flex;align-items:center;gap:10px;margin:0 0 10px;font-size:18px;font-weight:900}} +.step-no{{display:inline-grid;place-items:center;width:30px;height:30px;border:3px solid var(--ink);background:var(--yellow);font-weight:900}} pre{{white-space:pre-wrap;background:#121212;color:#fff;padding:13px;border:4px solid var(--ink);max-height:420px;overflow:auto;box-shadow:5px 5px 0 var(--yellow)}} .friend-links{{margin-top:18px;padding-top:12px;border-top:3px solid var(--ink);display:flex;gap:8px;align-items:center;flex-wrap:wrap}} .friend-links b{{font-size:12px;font-weight:900;text-transform:uppercase}} @@ -2842,7 +2959,7 @@ pre{{white-space:pre-wrap;background:#121212;color:#fff;padding:13px;border:4px @media (prefers-reduced-motion: reduce){{ *,*::before,*::after{{animation:none!important;transition:none!important}} }} -

{html_escape(title)}

WatchBot Panel
+

{html_escape(title)}

WatchBot Panel
{body}
""" @@ -3387,15 +3504,56 @@ HostLoc|https://hostloc.com|VPS,补货,优惠""" cleanup = (cfg_load_fresh().get("cleanup") or {}) bot_ready = bool(v["TELEGRAM_BOT_TOKEN"].strip() and v["ADMIN_CHAT_ID"].strip()) status = "" if bot_ready else "
未填写 Token 或管理员 ID;网页可用,但 Bot 和监控推送不可用。
" - body = f"""

Bot / 面板设置

{status}
- - -

TG 用户会话(可选)

仅用于“TG 群监听 -> 监听来源=用户会话”,适合 Bot 无法加入的群。填写后需重启。

+ login_row = telegram_login_status_row() + login_status = "已登录" if login_row.get("status") == "authorized" else "未登录" + login_user = login_row.get("username") or login_row.get("phone") or login_row.get("user_id") or "-" + body = f"""

设置向导

{status}
+
1Bot 基础配置
+

先保证 Bot 能给管理员发通知。

+
+
+
2TG 用户会话登录
+

用于频道媒体转发。先填 TG_API_ID / TG_API_HASH 并保存,再点二维码登录。

- + +
登录状态:{html_escape(login_status)} · 账号:{html_escape(login_user)}
+
+ +
+
3高级设置
+

一般保持默认即可。

-

监控数据自动清理

删除过期监控通知消息,并清理 RSS/网站监控状态和去重记录;不会删除用户、收件箱、双向对话消息。

-
改 Token、管理员 ID 或端口后需要重启。
""" +

自动清理

+
+
改 Token、管理员 ID、端口或 TG_API_ID / TG_API_HASH 后需要保存并重启。
+""" return layout("设置", body) def save_panel_settings( @@ -3556,6 +3714,53 @@ HostLoc|https://hostloc.com|VPS,补货,优惠""" ) return layout("已保存", "
已保存,不会自动重启;修改 Token、管理员 ID、端口、账号或密码后请重启。

返回用户管理 重启机器人

") + @app.post("/api/tg-login/qr") + async def api_tg_login_qr(_: str = Depends(panel_auth)) -> dict[str, Any]: + result = await telegram_login_prepare_qr() + if not result.get("ok"): + return {"ok": False, "error": result.get("error", "failed")} + login_id = secrets.token_urlsafe(8) + telegram_qr_logins[login_id] = { + "client": result["client"], + "login": result["login"], + "created_at": time.time(), + "status": "pending", + } + async def waiter(): + try: + data = await telegram_login_complete(result["client"], result["login"]) + telegram_qr_logins[login_id]["status"] = "authorized" + telegram_qr_logins[login_id]["result"] = data + except Exception as e: + telegram_qr_logins[login_id]["status"] = "error" + telegram_qr_logins[login_id]["error"] = str(e) + asyncio.create_task(waiter()) + return {"ok": True, "login_id": login_id, "qr_png": result["qr_png"]} + + @app.get("/api/tg-login/status") + async def api_tg_login_status(_: str = Depends(panel_auth), login_id: str = "") -> dict[str, Any]: + item = telegram_qr_logins.get(login_id) + if not item: + return {"ok": False, "status": "missing"} + status = item.get("status", "pending") + if time.time() - float(item.get("created_at", 0)) > 120: + item["status"] = "expired" + status = "expired" + result = item.get("result", {}) + return { + "ok": True, + "status": status, + "error": item.get("error", ""), + "username": result.get("username", ""), + "phone": result.get("phone", ""), + "user_id": result.get("user_id", ""), + } + + @app.post("/api/tg-login/logout") + async def api_tg_login_logout(_: str = Depends(panel_auth)) -> dict[str, Any]: + clear_telegram_login_session() + return {"ok": True} + @app.post("/users/{user_id}/note") async def user_note_save(user_id: int, _: str = Depends(panel_auth), note: str = Form("")) -> RedirectResponse: set_note(user_id, note.strip()) diff --git a/requirements.txt b/requirements.txt index 122a39e..4491c14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ python-multipart==0.0.20 PyYAML==6.0.3 uvicorn[standard]==0.38.0 telethon==1.42.0 +qrcode[pil]==8.2 From 6036dd2d61548961c394e9fa53b816da77e49bfb Mon Sep 17 00:00:00 2001 From: InfernoXuaI <1391197588@qq.com> Date: Sun, 31 May 2026 12:20:37 +0800 Subject: [PATCH 2/2] dockerbug --- README.md | 10 ++++++++++ app.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8f8c44a..ef73903 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ tg-watchbot 是一个轻量级 Python 服务,把 **Telegram 双向客服机器 - 支持媒体类型过滤:可选视频、文档、图片、音频。 - 支持 SOCKS5/HTTP 代理,适合国内服务器。 - 新增 Telegram 二维码登录:设置页填写 `TG_API_ID` / `TG_API_HASH` 后,可扫码生成并保存用户会话。 +- 修复 Docker / FastAPI 启动报错:移除 `RedirectResponse | HTMLResponse` 联合返回注解,避免被 FastAPI 当成 Pydantic response model 解析。 - 内置下载到服务器、断点续传、并发下载等功能,后续可通过配置开启。 - 仍兼容手动填写 `TG_API_SESSION`。 @@ -162,6 +163,8 @@ tg-watchbot 是一个轻量级 Python 服务,把 **Telegram 双向客服机器 - [`feedparser`](https://github.com/kurtmckee/feedparser):RSS/Atom 解析。 - [`Beautiful Soup`](https://www.crummy.com/software/BeautifulSoup/):HTML 解析和 CSS selector 抽取。 - [`PyYAML`](https://pyyaml.org/):`config.yaml` 配置读写。 +- [`telethon`](https://github.com/LonamiWebs/Telethon):Telegram 用户会话、群监听、二维码登录。 +- [`qrcode`](https://github.com/lincolnloop/python-qrcode):生成 Telegram 二维码登录图片。 - [`python-dotenv`](https://github.com/theskumar/python-dotenv):读取 `.env`。 - Python 标准库 `sqlite3`:消息、用户、去重、监控状态持久化。 @@ -201,9 +204,16 @@ docker compose logs -f 修改配置后重启: ```bash +docker compose up -d --build docker compose restart ``` +如果只是更新代码,建议直接重新构建并重启容器: + +```bash +docker compose up -d --build +``` + ## 手动安装(Python) diff --git a/app.py b/app.py index 80d35cc..fd47871 100644 --- a/app.py +++ b/app.py @@ -3198,7 +3198,7 @@ def create_panel_app() -> FastAPI: ai_prompt: str, ai_min_interval_seconds: str, ai_dedupe_window_seconds: str, - ) -> RedirectResponse | HTMLResponse: + ) -> Response: cfg = cfg_load_fresh() rows = cfg.setdefault("group_monitors", []) if not isinstance(rows, list): @@ -3264,7 +3264,7 @@ def create_panel_app() -> FastAPI: ai_prompt: str = Form(""), ai_min_interval_seconds: str = Form(str(DEFAULT_GROUP_AI_MIN_INTERVAL_SECONDS)), ai_dedupe_window_seconds: str = Form(str(DEFAULT_GROUP_AI_DEDUPE_WINDOW_SECONDS)), - ) -> RedirectResponse | HTMLResponse: + ) -> Response: return await save_group_monitor_common( None, name, @@ -3307,7 +3307,7 @@ def create_panel_app() -> FastAPI: ai_prompt: str = Form(""), ai_min_interval_seconds: str = Form(str(DEFAULT_GROUP_AI_MIN_INTERVAL_SECONDS)), ai_dedupe_window_seconds: str = Form(str(DEFAULT_GROUP_AI_DEDUPE_WINDOW_SECONDS)), - ) -> RedirectResponse | HTMLResponse: + ) -> Response: return await save_group_monitor_common( original_index, name,