自身群监控
自身群监控
This commit is contained in:
@@ -32,14 +32,13 @@ tg-watchbot 是一个轻量级 Python 服务,把 **Telegram 双向客服机器
|
||||
|
||||
### 2026-05-28 更新
|
||||
|
||||
- 新增「频道媒体下载」功能:使用 Telethon 用户账号登录 Telegram,可浏览已加入的群组/频道,选择性下载媒体文件。
|
||||
- 面板新增「频道媒体」页面:支持搜索群组、添加/暂停/恢复/删除监控、查看下载记录。
|
||||
- 支持断点续传:大文件下载中断后自动续传,不重复下载。
|
||||
- 支持并发下载控制:可设置同时下载数(默认 3),避免带宽打满。
|
||||
- 支持 SOCKS5/HTTP 代理:国内用户可配置代理访问 Telegram。
|
||||
- 支持按日期范围过滤:只下载指定时间段内的消息。
|
||||
- 支持关键词过滤、媒体类型过滤(视频/文档/图片/音频)、文件大小限制。
|
||||
- 下载完成可自动推送 Telegram 通知给管理员。
|
||||
- 新增「频道媒体转发」:使用 Telethon 用户账号登录 TG,实时转发群组/频道消息到你的 Telegram。
|
||||
- 面板新增「频道媒体」页面:搜索已加入群组,一键添加转发监控。
|
||||
- 支持暂停/恢复监控(保留配置)、删除监控。
|
||||
- 支持关键词过滤:只转发包含特定关键词的消息,留空则转发全部。
|
||||
- 支持媒体类型过滤:可选视频、文档、图片、音频。
|
||||
- 支持 SOCKS5/HTTP 代理,适合国内服务器。
|
||||
- 内置下载到服务器、断点续传、并发下载等功能,后续可通过配置开启。
|
||||
- 需要在设置页填写 `TG_API_ID`、`TG_API_HASH`、`TG_API_SESSION` 后使用。
|
||||
|
||||
### 2026-05-22 更新
|
||||
@@ -125,6 +124,7 @@ tg-watchbot 是一个轻量级 Python 服务,把 **Telegram 双向客服机器
|
||||
- 支持 SOCKS5/HTTP 代理,适合国内服务器使用。
|
||||
- 支持按日期范围过滤:只下载指定时间段内的消息。
|
||||
- 支持关键词过滤、媒体类型选择(视频/文档/图片/音频)、文件大小限制。
|
||||
- 支持实时转发模式:群消息匹配后直接转发到你的 Telegram(含视频/文档原文),无需下载到服务器。
|
||||
- 下载完成可自动推送 Telegram 通知给管理员。
|
||||
- 需要在设置页填写 `TG_API_ID`、`TG_API_HASH`、`TG_API_SESSION`。
|
||||
|
||||
|
||||
@@ -250,6 +250,8 @@ def init_db() -> None:
|
||||
date_from TEXT DEFAULT '',
|
||||
date_to TEXT DEFAULT '',
|
||||
max_concurrent INTEGER DEFAULT 3,
|
||||
forward_mode INTEGER DEFAULT 0,
|
||||
forward_to TEXT DEFAULT 'admin',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
@@ -277,6 +279,8 @@ def init_db() -> None:
|
||||
"ALTER TABLE channel_media_monitors ADD COLUMN date_from TEXT DEFAULT ''",
|
||||
"ALTER TABLE channel_media_monitors ADD COLUMN date_to TEXT DEFAULT ''",
|
||||
"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'",
|
||||
]:
|
||||
try:
|
||||
conn.execute(sql)
|
||||
@@ -647,6 +651,8 @@ def channel_media_monitor_create(
|
||||
date_from: str = "",
|
||||
date_to: str = "",
|
||||
max_concurrent: int = 3,
|
||||
forward_mode: bool = False,
|
||||
forward_to: str = "admin",
|
||||
) -> int:
|
||||
ts = now_iso()
|
||||
with closing(db()) as conn:
|
||||
@@ -654,11 +660,12 @@ def channel_media_monitor_create(
|
||||
"""INSERT INTO channel_media_monitors
|
||||
(channel_id, channel_title, channel_username, status, media_types, keywords,
|
||||
max_file_size_mb, download_dir, notify_telegram, proxy, date_from, date_to,
|
||||
max_concurrent, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
max_concurrent, forward_mode, forward_to, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(channel_id, channel_title, channel_username, "active", media_types, keywords,
|
||||
max_file_size_mb, download_dir, 1 if notify_telegram else 0,
|
||||
proxy, date_from, date_to, max_concurrent, ts, ts),
|
||||
proxy, date_from, date_to, max_concurrent, 1 if forward_mode else 0,
|
||||
forward_to, ts, ts),
|
||||
)
|
||||
conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
@@ -667,7 +674,7 @@ def channel_media_monitor_create(
|
||||
def channel_media_monitor_update(monitor_id: int, **kwargs: Any) -> None:
|
||||
allowed = {"status", "media_types", "keywords", "max_file_size_mb", "download_dir",
|
||||
"last_message_id", "notify_telegram", "total_downloaded", "total_size_bytes",
|
||||
"proxy", "date_from", "date_to", "max_concurrent"}
|
||||
"proxy", "date_from", "date_to", "max_concurrent", "forward_mode", "forward_to"}
|
||||
updates = []
|
||||
values = []
|
||||
for k, v in kwargs.items():
|
||||
@@ -871,6 +878,92 @@ async def channel_media_monitor_loop() -> None:
|
||||
await disconnect_channel_media_client()
|
||||
|
||||
|
||||
async def channel_media_forward_listener() -> None:
|
||||
"""Real-time listener: forward messages from monitored groups to admin Telegram."""
|
||||
if TelegramClient is None or StringSession is None:
|
||||
logger.info("channel media forward listener skipped: telethon not installed")
|
||||
return
|
||||
api_id_raw, api_hash, session = user_session_config()
|
||||
if not api_id_raw or not api_hash or not session:
|
||||
logger.info("channel media forward listener skipped: user session not configured")
|
||||
return
|
||||
try:
|
||||
api_id = int(api_id_raw)
|
||||
except Exception:
|
||||
return
|
||||
try:
|
||||
client = TelegramClient(StringSession(session), api_id, api_hash)
|
||||
|
||||
def _get_forward_monitors() -> dict[int, dict[str, Any]]:
|
||||
monitors = channel_media_monitors_all()
|
||||
result = {}
|
||||
for m in monitors:
|
||||
if m.get("status") != "active" or not m.get("forward_mode"):
|
||||
continue
|
||||
try:
|
||||
cid = int(m["channel_id"])
|
||||
result[cid] = m
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return result
|
||||
|
||||
@client.on(events.NewMessage(incoming=True))
|
||||
async def on_new_message_for_forward(event: Any) -> None:
|
||||
try:
|
||||
chat_id = getattr(event, "chat_id", None)
|
||||
if chat_id is None:
|
||||
return
|
||||
fwd_monitors = _get_forward_monitors()
|
||||
monitor = fwd_monitors.get(int(chat_id))
|
||||
if not monitor:
|
||||
return
|
||||
msg = event.message
|
||||
text = (msg.text or "") + " " + (msg.caption or "")
|
||||
# Keyword filter
|
||||
keywords_str = str(monitor.get("keywords") or "").strip()
|
||||
if keywords_str:
|
||||
keywords_list = [k.strip() for k in keywords_str.split(",") if k.strip()]
|
||||
if keywords_list and not any(k.lower() in text.lower() for k in keywords_list):
|
||||
return
|
||||
# Media type filter
|
||||
media_types_str = str(monitor.get("media_types") or "").strip()
|
||||
if media_types_str:
|
||||
allowed = {t.strip().lower() for t in media_types_str.split(",") if t.strip()}
|
||||
if allowed and not msg.media:
|
||||
has_text_only = bool(text.strip())
|
||||
if not has_text_only:
|
||||
return
|
||||
# Determine forward target
|
||||
forward_to = str(monitor.get("forward_to") or "admin").strip()
|
||||
if forward_to == "admin":
|
||||
targets = all_admin_chat_ids()
|
||||
elif forward_to == "saved":
|
||||
targets = ["me"]
|
||||
else:
|
||||
try:
|
||||
targets = [int(forward_to)]
|
||||
except (TypeError, ValueError):
|
||||
targets = all_admin_chat_ids()
|
||||
# Forward the message
|
||||
for target in targets:
|
||||
try:
|
||||
await msg.forward_to(target)
|
||||
except Exception:
|
||||
logger.exception("forward failed target=%s msg_id=%s", target, msg.id)
|
||||
logger.info("forwarded message from chat=%s msg_id=%s to %s", chat_id, msg.id, targets)
|
||||
except Exception:
|
||||
logger.exception("on_new_message_for_forward error")
|
||||
|
||||
await client.start()
|
||||
logger.info("channel media forward listener started")
|
||||
await client.run_until_disconnected()
|
||||
except asyncio.CancelledError:
|
||||
logger.info("channel media forward listener cancelled")
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("channel media forward listener crashed")
|
||||
|
||||
|
||||
async def telethon_download_from_channel(monitor_id: int, download_history: bool = False) -> int:
|
||||
monitor = channel_media_monitor_get(monitor_id)
|
||||
if not monitor:
|
||||
@@ -3688,8 +3781,6 @@ HostLoc|https://hostloc.com|VPS,补货,优惠"""
|
||||
badge_html = '<span class="badge" style="background:#eab308">已暂停</span>'
|
||||
else:
|
||||
badge_html = '<span class="badge" style="background:#6b7280">已停止</span>'
|
||||
total = m.get("total_downloaded", 0)
|
||||
size_mb = (m.get("total_size_bytes", 0) or 0) // 1024 // 1024
|
||||
username = f"@{m.get('channel_username', '')}" if m.get("channel_username") else ""
|
||||
pause_btn = ""
|
||||
if status == "active":
|
||||
@@ -3697,22 +3788,17 @@ HostLoc|https://hostloc.com|VPS,补货,优惠"""
|
||||
elif status == "paused":
|
||||
pause_btn = f"<a class='btn ok' href='/channel-media/{m['id']}/resume'>恢复</a>"
|
||||
proxy_info = " · 代理: " + html_escape(m.get("proxy", "")) if m.get("proxy") else ""
|
||||
date_info = ""
|
||||
if m.get("date_from") or m.get("date_to"):
|
||||
date_info = " · 日期: " + html_escape(m.get("date_from", "")) + "~" + html_escape(m.get("date_to", ""))
|
||||
cards_html += f"""<div class=card style='margin:12px 0'>
|
||||
<div style='display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px'>
|
||||
<div><h3 style='margin:0 0 4px'>{badge_html} {html_escape(m.get('channel_title',''))} {html_escape(username)}</h3>
|
||||
<small class=muted>chat_id: {m.get('channel_id','')} · 媒体类型: {html_escape(m.get('media_types',''))} · 并发: {m.get('max_concurrent',3)} · 已下载: {total} 个 · {size_mb} MB{proxy_info}{date_info}</small></div>
|
||||
<small class=muted>chat_id: {m.get('channel_id','')} · 转发: {'开启' if m.get('forward_mode') else '关闭'}{proxy_info}</small></div>
|
||||
<div class=actions>
|
||||
{pause_btn}
|
||||
<a class='btn ok' href='/channel-media/{m['id']}/download'>下载历史</a>
|
||||
<a class='btn' href='/channel-media/{m['id']}/check'>立即检查</a>
|
||||
<a class='btn danger' href='/channel-media/{m['id']}/delete' onclick='return confirm("确定删除该监控?")'>删除</a>
|
||||
</div></div></div>"""
|
||||
return f"""<div class=card>
|
||||
<div class=toolbar><div><h2 style='margin:0 0 6px'>频道媒体监控</h2>
|
||||
<p class=muted style='margin:0'>使用你的 TG 账号监控频道/群组,自动下载媒体文件。</p></div>
|
||||
<div class=toolbar><div><h2 style='margin:0 0 6px'>频道媒体转发</h2>
|
||||
<p class=muted style='margin:0'>使用你的 TG 账号监听群组,消息实时转发到你的 Telegram。</p></div>
|
||||
<div class=actions><button class='btn primary' onclick='document.getElementById("addModal").style.display="flex"'>添加频道/群组</button></div></div>
|
||||
{notice}{cards_html}</div>
|
||||
|
||||
@@ -3733,25 +3819,17 @@ HostLoc|https://hostloc.com|VPS,补货,优惠"""
|
||||
<input type=hidden id='selChannelUsername' name='channel_username'>
|
||||
</div>
|
||||
<div style='margin-top:14px'>
|
||||
<label>媒体类型(逗号分隔)</label>
|
||||
<input id='mediaTypes' value='video,document' placeholder='video,document,photo,audio'>
|
||||
<label>关键词过滤(逗号分隔,可留空)</label>
|
||||
<input id='mediaKeywords' placeholder='留空则下载所有'>
|
||||
<label>最大文件大小 (MB)</label>
|
||||
<input id='maxFileSize' type=number value=2000 min=1>
|
||||
<label>下载目录(留空使用默认)</label>
|
||||
<input id='downloadDir' placeholder='留空则使用默认目录'>
|
||||
<label>关键词过滤(逗号分隔,留空则转发所有消息)</label>
|
||||
<input id='mediaKeywords' placeholder='留空则转发所有消息'>
|
||||
<label>媒体类型过滤(逗号分隔,留空不限)</label>
|
||||
<input id='mediaTypes' value='' placeholder='video,document,photo,audio(留空=所有类型)'>
|
||||
<label>代理(留空不用,支持 socks5://host:port 或 http://host:port)</label>
|
||||
<input id='mediaProxy' placeholder='socks5://127.0.0.1:1080'>
|
||||
<div class=grid><div><label>开始日期(留空不限)</label>
|
||||
<input id='dateFrom' type=date></div>
|
||||
<div><label>结束日期(留空不限)</label>
|
||||
<input id='dateTo' type=date></div></div>
|
||||
<label>最大并发下载数</label>
|
||||
<input id='maxConcurrent' type=number value=3 min=1 max=10>
|
||||
<div class=check-row style='margin-top:8px'>
|
||||
<label><input type=checkbox id='notifyTg' checked> 下载通知推送到 Telegram</label>
|
||||
<label><input type=checkbox id='forwardMode' checked> 实时转发到 Telegram</label>
|
||||
</div>
|
||||
<div><label>转发目标</label>
|
||||
<select id='forwardTo'><option value='admin'>管理员</option><option value='saved'>我的收藏(Saved Messages)</option></select></div>
|
||||
</div>
|
||||
<div class=form-actions>
|
||||
<button class='btn primary' onclick='addMonitor()'>添加监控</button>
|
||||
@@ -3803,13 +3881,9 @@ async function addMonitor() {{
|
||||
channel_username: document.getElementById('selChannelUsername').value,
|
||||
media_types: document.getElementById('mediaTypes').value,
|
||||
keywords: document.getElementById('mediaKeywords').value,
|
||||
max_file_size_mb: document.getElementById('maxFileSize').value,
|
||||
download_dir: document.getElementById('downloadDir').value,
|
||||
proxy: document.getElementById('mediaProxy').value,
|
||||
date_from: document.getElementById('dateFrom').value,
|
||||
date_to: document.getElementById('dateTo').value,
|
||||
max_concurrent: document.getElementById('maxConcurrent').value,
|
||||
notify_telegram: document.getElementById('notifyTg').checked ? 'on' : '',
|
||||
forward_mode: document.getElementById('forwardMode').checked ? 'on' : '',
|
||||
forward_to: document.getElementById('forwardTo').value,
|
||||
}});
|
||||
try {{
|
||||
const resp = await fetch('/api/monitors/create', {{ method: 'POST', headers: {{'Content-Type':'application/x-www-form-urlencoded'}}, body: body.toString() }});
|
||||
@@ -3850,6 +3924,8 @@ document.getElementById('addModal').addEventListener('click', function(e) {{ if
|
||||
date_from: str = Form(""),
|
||||
date_to: str = Form(""),
|
||||
max_concurrent: int = Form(3),
|
||||
forward_mode: str | None = Form(None),
|
||||
forward_to: str = Form("admin"),
|
||||
) -> RedirectResponse:
|
||||
try:
|
||||
cid = int(channel_id.strip())
|
||||
@@ -3868,6 +3944,8 @@ document.getElementById('addModal').addEventListener('click', function(e) {{ if
|
||||
date_from.strip(),
|
||||
date_to.strip(),
|
||||
max(1, min(10, max_concurrent)),
|
||||
bool(forward_mode),
|
||||
forward_to.strip() or "admin",
|
||||
)
|
||||
return RedirectResponse("/channel-media", status_code=303)
|
||||
|
||||
@@ -3960,7 +4038,6 @@ async def main_async(run_once: bool = False, panel_only: bool = False) -> None:
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
if run_once:
|
||||
# If .env is filled, send notifications during manual test; otherwise just log.
|
||||
try:
|
||||
token, admin_chat_id = validate_env()
|
||||
admin_chat_ids = parse_admin_chat_ids(os.getenv("ADMIN_CHAT_ID", ""))
|
||||
@@ -3999,9 +4076,10 @@ async def main_async(run_once: bool = False, panel_only: bool = False) -> None:
|
||||
else:
|
||||
user_session_listener_task = asyncio.create_task(run_user_session_group_listener())
|
||||
if TelegramClient is not None and user_session_ready():
|
||||
asyncio.create_task(channel_media_monitor_loop())
|
||||
asyncio.create_task(channel_media_forward_listener())
|
||||
logger.info("channel media forward listener started")
|
||||
else:
|
||||
logger.info("channel media monitor loop skipped: telethon not installed or user session not configured")
|
||||
logger.info("channel media forward listener skipped: telethon not installed or user session not configured")
|
||||
await admin_send(f"tg-watchbot 已启动\n时间:{now_iso()}")
|
||||
logger.info("bot polling start")
|
||||
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
total = monitor.get("total_downloaded", 0)
|
||||
size_mb = (monitor.get("total_size_bytes", 0) or 0) // 1024 // 1024
|
||||
body = f"""<div class=card><h2>下载记录 - {html_escape(monitor.get('channel_title',''))}</h2>
|
||||
<p class=muted>累计:{total} 个文件,{size_mb} MB</p>
|
||||
<div class=actions style='margin-bottom:16px'>
|
||||
<a class='btn ok' href='/channel-media/{monitor_id}/check'>立即下载新内容</a>
|
||||
<a class='btn' href='/channel-media'>返回列表</a></div>
|
||||
<table><tr><th>ID</th><th>类型</th><th>文件名/说明</th><th>大小</th><th>时间</th></tr>{rows}</table></div>"""
|
||||
return layout("下载记录", body)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
async def start_panel_server() -> uvicorn.Server | None:
|
||||
if not panel_enabled():
|
||||
logger.info("web panel disabled")
|
||||
return None
|
||||
host = os.getenv("WEB_PANEL_HOST", "127.0.0.1")
|
||||
port = int(os.getenv("WEB_PANEL_PORT", "8765"))
|
||||
server = uvicorn.Server(uvicorn.Config(create_panel_app(), host=host, port=port, log_level="info"))
|
||||
asyncio.create_task(server.serve())
|
||||
logger.info("web panel listening on http://%s:%s", host, port)
|
||||
return server
|
||||
|
||||
def validate_env() -> tuple[str, int]:
|
||||
load_dotenv(ENV_PATH)
|
||||
token = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
|
||||
admin = os.getenv("ADMIN_CHAT_ID", "").strip()
|
||||
if not token:
|
||||
raise RuntimeError(f"TELEGRAM_BOT_TOKEN is missing in {ENV_PATH}")
|
||||
if not admin:
|
||||
raise RuntimeError(f"ADMIN_CHAT_ID is missing in {ENV_PATH}")
|
||||
ids = parse_admin_chat_ids(admin)
|
||||
if not ids:
|
||||
raise RuntimeError(f"ADMIN_CHAT_ID is invalid in {ENV_PATH}")
|
||||
return token, ids[0]
|
||||
|
||||
|
||||
def bot_env_configured() -> bool:
|
||||
load_dotenv(ENV_PATH, override=True)
|
||||
return bool(os.getenv("TELEGRAM_BOT_TOKEN", "").strip() and os.getenv("ADMIN_CHAT_ID", "").strip())
|
||||
|
||||
|
||||
async def main_async(run_once: bool = False, panel_only: bool = False) -> None:
|
||||
global bot, admin_chat_id, admin_chat_ids, config, scheduler_ref, user_session_listener_task
|
||||
load_dotenv(ENV_PATH, override=True)
|
||||
config = load_config()
|
||||
setup_logging(os.getenv("LOG_LEVEL", "INFO"))
|
||||
init_db()
|
||||
if panel_only:
|
||||
await start_panel_server()
|
||||
logger.info("panel-only mode start")
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
if run_once:
|
||||
try:
|
||||
token, admin_chat_id = validate_env()
|
||||
admin_chat_ids = parse_admin_chat_ids(os.getenv("ADMIN_CHAT_ID", ""))
|
||||
bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||
except Exception as e:
|
||||
logger.warning("run-once without Telegram notification: %s", e)
|
||||
await run_all_monitors_once()
|
||||
if bot:
|
||||
await bot.session.close()
|
||||
return
|
||||
await start_panel_server()
|
||||
if not bot_env_configured():
|
||||
logger.warning(
|
||||
"Telegram bot is not configured. Web panel is available, but Telegram polling, monitor notifications, and admin/user messaging will not work until TELEGRAM_BOT_TOKEN and ADMIN_CHAT_ID are saved, then the service is restarted."
|
||||
)
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
token, admin_chat_id = validate_env()
|
||||
admin_chat_ids = parse_admin_chat_ids(os.getenv("ADMIN_CHAT_ID", ""))
|
||||
bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||
dp = Dispatcher()
|
||||
dp.include_router(router)
|
||||
scheduler = AsyncIOScheduler(timezone="Asia/Shanghai")
|
||||
scheduler_ref = scheduler
|
||||
schedule_monitors(scheduler)
|
||||
scheduler.start()
|
||||
asyncio.create_task(flush_pending_loop())
|
||||
asyncio.create_task(cleanup_monitor_loop())
|
||||
if group_monitors_need_user_session():
|
||||
if TelegramClient is None:
|
||||
logger.warning("group monitor with listen_source=user_session detected, but telethon is not installed")
|
||||
elif not user_session_ready():
|
||||
logger.warning(
|
||||
"group monitor with listen_source=user_session detected, but TG_API_ID/TG_API_HASH/TG_API_SESSION is not complete"
|
||||
)
|
||||
else:
|
||||
user_session_listener_task = asyncio.create_task(run_user_session_group_listener())
|
||||
if TelegramClient is not None and user_session_ready():
|
||||
asyncio.create_task(channel_media_monitor_loop())
|
||||
asyncio.create_task(channel_media_forward_listener())
|
||||
logger.info("channel media download + forward listeners started")
|
||||
else:
|
||||
logger.info("channel media listeners skipped: telethon not installed or user session not configured")
|
||||
await admin_send(f"tg-watchbot 已启动\n时间:{now_iso()}")
|
||||
logger.info("bot polling start")
|
||||
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--run-once", action="store_true", help="run all monitors once and exit; does not need Telegram token unless notification is sent")
|
||||
parser.add_argument("--panel-only", action="store_true", help="start only the web admin panel, useful before Telegram token is configured")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
asyncio.run(main_async(run_once=args.run_once, panel_only=args.panel_only))
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("fatal error")
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
document.getElementById('addModal').addEventListener('click', function(e) { if (e.target === this) this.style.display='none'; });
|
||||
</script>"""
|
||||
|
||||
@app.get("/channel-media", response_class=HTMLResponse)
|
||||
async def channel_media_page(_: str = Depends(panel_auth)) -> str:
|
||||
return layout("频道媒体", channel_media_page_html())
|
||||
|
||||
@app.get("/api/groups")
|
||||
async def api_groups(_: str = Depends(panel_auth), q: str = "", limit: int = 500) -> dict[str, Any]:
|
||||
if not user_session_ready():
|
||||
return {"groups": [], "error": "TG user session not configured"}
|
||||
if q.strip():
|
||||
groups = await telethon_search_dialogs(q.strip(), limit=min(limit, 200))
|
||||
else:
|
||||
groups = await telethon_list_dialogs(limit=min(limit, 500))
|
||||
groups = [g for g in groups if g.get("type") in ("group", "channel")]
|
||||
return {"groups": groups}
|
||||
|
||||
@app.post("/api/monitors/create")
|
||||
async def api_monitor_create(
|
||||
_: str = Depends(panel_auth),
|
||||
channel_id: str = Form(...),
|
||||
channel_title: str = Form(""),
|
||||
channel_username: str = Form(""),
|
||||
media_types: str = Form("video,document"),
|
||||
keywords: str = Form(""),
|
||||
max_file_size_mb: int = Form(2000),
|
||||
download_dir: str = Form(""),
|
||||
notify_telegram: str | None = Form(None),
|
||||
proxy: str = Form(""),
|
||||
date_from: str = Form(""),
|
||||
date_to: str = Form(""),
|
||||
max_concurrent: int = Form(3),
|
||||
forward_mode: str | None = Form(None),
|
||||
forward_to: str = Form("admin"),
|
||||
) -> RedirectResponse:
|
||||
try:
|
||||
cid = int(channel_id.strip())
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(400, "invalid channel_id")
|
||||
channel_media_monitor_create(
|
||||
cid,
|
||||
channel_title.strip() or str(cid),
|
||||
channel_username.strip(),
|
||||
media_types.strip() or "video,document",
|
||||
keywords.strip(),
|
||||
max(1, max_file_size_mb),
|
||||
download_dir.strip(),
|
||||
bool(notify_telegram),
|
||||
proxy.strip(),
|
||||
date_from.strip(),
|
||||
date_to.strip(),
|
||||
max(1, min(10, max_concurrent)),
|
||||
bool(forward_mode),
|
||||
forward_to.strip() or "admin",
|
||||
)
|
||||
return RedirectResponse("/channel-media", status_code=303)
|
||||
|
||||
@app.get("/channel-media/{monitor_id}/pause")
|
||||
async def channel_media_pause(monitor_id: int, _: str = Depends(panel_auth)) -> RedirectResponse:
|
||||
channel_media_monitor_update(monitor_id, status="paused")
|
||||
return RedirectResponse("/channel-media", status_code=303)
|
||||
|
||||
@app.get("/channel-media/{monitor_id}/resume")
|
||||
async def channel_media_resume(monitor_id: int, _: str = Depends(panel_auth)) -> RedirectResponse:
|
||||
channel_media_monitor_update(monitor_id, status="active")
|
||||
return RedirectResponse("/channel-media", status_code=303)
|
||||
|
||||
@app.get("/channel-media/{monitor_id}/delete")
|
||||
async def channel_media_delete_route(monitor_id: int, _: str = Depends(panel_auth)) -> RedirectResponse:
|
||||
channel_media_monitor_delete(monitor_id)
|
||||
return RedirectResponse("/channel-media", status_code=303)
|
||||
|
||||
@app.get("/channel-media/{monitor_id}/check", response_class=HTMLResponse)
|
||||
async def channel_media_check(monitor_id: int, _: str = Depends(panel_auth)) -> str:
|
||||
count = await telethon_download_from_channel(monitor_id)
|
||||
monitor = channel_media_monitor_get(monitor_id)
|
||||
name = monitor.get("channel_title", "") if monitor else ""
|
||||
return layout("检查完成", f"<div class=msg>已检查频道 {html_escape(name)},新增 {count} 个文件。</div><p><a class=btn href='/channel-media'>返回</a></p>")
|
||||
|
||||
@app.get("/channel-media/{monitor_id}/download", response_class=HTMLResponse)
|
||||
async def channel_media_download_history(monitor_id: int, _: str = Depends(panel_auth)) -> str:
|
||||
monitor = channel_media_monitor_get(monitor_id)
|
||||
if not monitor:
|
||||
raise HTTPException(404)
|
||||
downloads = channel_media_downloads_list(monitor_id, limit=200)
|
||||
rows = ""
|
||||
for d in downloads:
|
||||
size_mb = f"{(d.get('file_size', 0) or 0) / 1024 / 1024:.1f} MB"
|
||||
rows += f"<tr><td>{d.get('id')}</td><td>{html_escape(d.get('media_type',''))}</td>"
|
||||
rows += f"<td>{html_escape(d.get('file_name',''))}<br><small class=muted>{html_escape(d.get('caption','')[:100])}</small></td>"
|
||||
rows += f"<td>{size_mb}</td><td><small>{html_escape(d.get('created_at',''))}</small></td></tr>"
|
||||
total = monitor.get("total_downloaded", 0)
|
||||
size_mb = (monitor.get("total_size_bytes", 0) or 0) // 1024 // 1024
|
||||
body = f"""<div class=card><h2>下载记录 - {html_escape(monitor.get('channel_title',''))}</h2>
|
||||
<p class=muted>累计:{total} 个文件,{size_mb} MB</p>
|
||||
<div class=actions style='margin-bottom:16px'>
|
||||
<a class='btn ok' href='/channel-media/{monitor_id}/check'>立即下载新内容</a>
|
||||
<a class='btn' href='/channel-media'>返回列表</a></div>
|
||||
<table><tr><th>ID</th><th>类型</th><th>文件名/说明</th><th>大小</th><th>时间</th></tr>{rows}</table></div>"""
|
||||
return layout("下载记录", body)
|
||||
|
||||
return app
|
||||
Reference in New Issue
Block a user