feat: Telegram user monitor bot
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
bot.session*
|
||||
__pycache__/
|
||||
*.pyc
|
||||
13
Dockerfile
Normal file
13
Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . ./
|
||||
|
||||
CMD ["python", "-u", "main.py"]
|
||||
56
README.md
Normal file
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Telegram 用户监听机器人
|
||||
|
||||
基于 **Pyrogram Userbot + Bot** 的监听与通知项目:
|
||||
- Userbot 监听所有群消息
|
||||
- Bot 负责规则配置与发送通知
|
||||
|
||||
## 功能
|
||||
- `/watch 群ID 用户ID 关键词1 关键词2` 添加监听规则
|
||||
- `/unwatch 群ID 用户ID` 删除规则
|
||||
- `/list` 查看所有规则
|
||||
- `/notify 目标ID` 设置通知目标(不设置则默认通知给你自己)
|
||||
|
||||
触发通知包含:群名、用户名、用户ID、关键词、消息内容。
|
||||
|
||||
## 运行前准备
|
||||
1. 在 https://my.telegram.org 申请 `api_id` 与 `api_hash`。
|
||||
2. 获取 Userbot 的会话字符串(Session String)。
|
||||
|
||||
生成 Session String 示例:
|
||||
```bash
|
||||
python - <<'PY'
|
||||
from pyrogram import Client
|
||||
|
||||
api_id = int(input("API_ID: "))
|
||||
api_hash = input("API_HASH: ")
|
||||
|
||||
with Client("user", api_id=api_id, api_hash=api_hash) as app:
|
||||
print(app.export_session_string())
|
||||
PY
|
||||
```
|
||||
|
||||
## 本地运行
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
|
||||
export TG_API_ID=123456
|
||||
export TG_API_HASH=your_api_hash
|
||||
export TG_BOT_TOKEN=123456:bot_token
|
||||
export TG_USER_SESSION_STRING=your_user_session_string
|
||||
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Docker 运行
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## 规则存储
|
||||
- 默认存储在 `./rules.json`
|
||||
- 可通过环境变量 `RULES_PATH` 自定义
|
||||
- 支持多用户独立配置
|
||||
|
||||
## 注意事项
|
||||
- 需要确保 Userbot 已加入目标群
|
||||
- Bot 需要能够向通知目标发送消息
|
||||
20
config.py
Normal file
20
config.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Telegram API 配置
|
||||
API_ID = int(os.getenv("TG_API_ID", "0"))
|
||||
API_HASH = os.getenv("TG_API_HASH", "")
|
||||
BOT_TOKEN = os.getenv("TG_BOT_TOKEN", "")
|
||||
USER_SESSION_STRING = os.getenv("TG_USER_SESSION_STRING", "")
|
||||
|
||||
# 规则文件路径
|
||||
RULES_PATH = Path(os.getenv("RULES_PATH", "./rules.json"))
|
||||
|
||||
# 日志等级
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||
|
||||
# 超级管理员(环境变量配置,不可被删除)
|
||||
SUPER_ADMIN_IDS = [int(x.strip()) for x in os.getenv("ADMIN_IDS", "").split(",") if x.strip()]
|
||||
|
||||
# 动态管理员文件路径
|
||||
ADMINS_PATH = Path(os.getenv("ADMINS_PATH", "./admins.json"))
|
||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
tg-user-monitor:
|
||||
build: .
|
||||
container_name: tg-user-monitor
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
TG_API_ID: "123456"
|
||||
TG_API_HASH: "your_api_hash"
|
||||
TG_BOT_TOKEN: "123456:bot_token"
|
||||
TG_USER_SESSION_STRING: "your_user_session_string"
|
||||
RULES_PATH: "/app/rules.json"
|
||||
LOG_LEVEL: "INFO"
|
||||
volumes:
|
||||
- ./rules.json:/app/rules.json
|
||||
7
gen_session.py
Normal file
7
gen_session.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from pyrogram import Client
|
||||
|
||||
api_id = int(input("API_ID: "))
|
||||
api_hash = input("API_HASH: ")
|
||||
|
||||
with Client("user", api_id=api_id, api_hash=api_hash) as app:
|
||||
print("Session 生成成功!")
|
||||
609
main.py
Normal file
609
main.py
Normal file
@@ -0,0 +1,609 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import Any, Deque, Dict, List, Set
|
||||
|
||||
from pyrogram import Client, filters, idle
|
||||
from pyrogram.errors import RPCError
|
||||
from pyrogram.handlers import MessageHandler
|
||||
|
||||
import config
|
||||
|
||||
DATA_LOCK = asyncio.Lock()
|
||||
DATA_CACHE: Dict[str, Any] = {"users": {}}
|
||||
|
||||
bot_client: Client | None = None
|
||||
user_client: Client | None = None
|
||||
|
||||
PROCESSED_ORDER: Dict[int, Deque[int]] = {}
|
||||
PROCESSED_SEEN: Dict[int, Set[int]] = {}
|
||||
MAX_PROCESSED_PER_CHAT = 1000
|
||||
POLL_INTERVAL_SECONDS = 10
|
||||
|
||||
ADMINS_CACHE: List[int] = []
|
||||
|
||||
|
||||
def _ensure_rules_file(path: Path) -> None:
|
||||
if not path.exists():
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps({"users": {}}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _load_data(path: Path) -> Dict[str, Any]:
|
||||
_ensure_rules_file(path)
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
if not raw.strip():
|
||||
return {"users": {}}
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
if "users" not in data or not isinstance(data["users"], dict):
|
||||
return {"users": {}}
|
||||
return data
|
||||
except json.JSONDecodeError:
|
||||
return {"users": {}}
|
||||
|
||||
|
||||
def _save_data(path: Path, data: Dict[str, Any]) -> None:
|
||||
tmp_path = path.with_suffix(".tmp")
|
||||
tmp_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
tmp_path.replace(path)
|
||||
|
||||
|
||||
def _get_user_bucket(data: Dict[str, Any], owner_id: int) -> Dict[str, Any]:
|
||||
key = str(owner_id)
|
||||
if key not in data["users"]:
|
||||
data["users"][key] = {"notify_targets": [], "rules": []}
|
||||
# 兼容旧数据:单个 notify_target 转为列表
|
||||
bucket = data["users"][key]
|
||||
if "notify_target" in bucket and "notify_targets" not in bucket:
|
||||
old_target = bucket.pop("notify_target")
|
||||
bucket["notify_targets"] = [old_target] if old_target else []
|
||||
if "notify_targets" not in bucket:
|
||||
bucket["notify_targets"] = []
|
||||
return bucket
|
||||
|
||||
|
||||
def _normalize_keywords(keywords: List[str]) -> List[str]:
|
||||
seen = set()
|
||||
result = []
|
||||
for kw in keywords:
|
||||
kw = kw.strip()
|
||||
if not kw:
|
||||
continue
|
||||
lowered = kw.lower()
|
||||
if lowered in seen:
|
||||
continue
|
||||
seen.add(lowered)
|
||||
result.append(kw)
|
||||
return result
|
||||
|
||||
|
||||
def _load_admins() -> List[int]:
|
||||
if not config.ADMINS_PATH.exists():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(config.ADMINS_PATH.read_text(encoding="utf-8"))
|
||||
return data.get("admins", [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _save_admins(admins: List[int]) -> None:
|
||||
config.ADMINS_PATH.write_text(
|
||||
json.dumps({"admins": admins}, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _get_all_admins() -> List[int]:
|
||||
return list(set(config.SUPER_ADMIN_IDS + ADMINS_CACHE))
|
||||
|
||||
|
||||
def _check_admin(user_id: int) -> bool:
|
||||
all_admins = _get_all_admins()
|
||||
if not all_admins:
|
||||
return True
|
||||
return user_id in all_admins
|
||||
|
||||
|
||||
def _is_super_admin(user_id: int) -> bool:
|
||||
return user_id in config.SUPER_ADMIN_IDS
|
||||
|
||||
|
||||
def _remember_message(chat_id: int, msg_id: int) -> bool:
|
||||
order = PROCESSED_ORDER.setdefault(chat_id, deque())
|
||||
seen = PROCESSED_SEEN.setdefault(chat_id, set())
|
||||
if msg_id in seen:
|
||||
return False
|
||||
order.append(msg_id)
|
||||
seen.add(msg_id)
|
||||
while len(order) > MAX_PROCESSED_PER_CHAT:
|
||||
oldest = order.popleft()
|
||||
seen.discard(oldest)
|
||||
return True
|
||||
|
||||
|
||||
def _keyword_hit(content_lower: str, keyword: str) -> bool:
|
||||
if keyword == "*":
|
||||
return True
|
||||
lowered = keyword.lower()
|
||||
if "*" not in lowered:
|
||||
return lowered in content_lower
|
||||
pattern = re.escape(lowered).replace("\\*", ".*")
|
||||
try:
|
||||
return re.search(pattern, content_lower) is not None
|
||||
except re.error:
|
||||
return lowered.replace("*", "") in content_lower
|
||||
|
||||
|
||||
async def cmd_watch(client: Client, message) -> None:
|
||||
if not message.from_user or not _check_admin(message.from_user.id):
|
||||
return
|
||||
args = message.text.split()
|
||||
if len(args) < 4:
|
||||
await message.reply_text("用法:/watch 群ID|* 用户ID|* 关键词|*\n* 表示匹配所有")
|
||||
return
|
||||
|
||||
group_id = None
|
||||
if args[1] != "*":
|
||||
try:
|
||||
group_id = int(args[1])
|
||||
except ValueError:
|
||||
await message.reply_text("群ID 必须是数字或 *")
|
||||
return
|
||||
|
||||
user_id = None
|
||||
if args[2] != "*":
|
||||
try:
|
||||
user_id = int(args[2])
|
||||
except ValueError:
|
||||
await message.reply_text("用户ID 必须是数字或 *")
|
||||
return
|
||||
|
||||
if args[3] == "*":
|
||||
keywords = ["*"]
|
||||
else:
|
||||
keywords = _normalize_keywords(args[3:])
|
||||
if not keywords:
|
||||
await message.reply_text("请提供至少一个关键词或 *")
|
||||
return
|
||||
|
||||
owner_id = message.from_user.id
|
||||
async with DATA_LOCK:
|
||||
bucket = _get_user_bucket(DATA_CACHE, owner_id)
|
||||
for rule in bucket["rules"]:
|
||||
if rule["group_id"] == group_id and rule["user_id"] == user_id and rule["keywords"] == keywords:
|
||||
await message.reply_text("规则已存在,无需重复添加。")
|
||||
return
|
||||
bucket["rules"].append({"group_id": group_id, "user_id": user_id, "keywords": keywords})
|
||||
_save_data(config.RULES_PATH, DATA_CACHE)
|
||||
|
||||
await message.reply_text("✅ 已添加监听规则。")
|
||||
|
||||
|
||||
async def cmd_unwatch(client: Client, message) -> None:
|
||||
if not message.from_user or not _check_admin(message.from_user.id):
|
||||
return
|
||||
args = message.text.split()
|
||||
if len(args) != 2:
|
||||
await message.reply_text("用法:/unwatch 序号\n例如:/unwatch 1")
|
||||
return
|
||||
|
||||
try:
|
||||
idx = int(args[1])
|
||||
except ValueError:
|
||||
await message.reply_text("序号必须是数字")
|
||||
return
|
||||
|
||||
owner_id = message.from_user.id
|
||||
async with DATA_LOCK:
|
||||
bucket = _get_user_bucket(DATA_CACHE, owner_id)
|
||||
if idx < 1 or idx > len(bucket["rules"]):
|
||||
await message.reply_text(f"序号无效,当前共 {len(bucket['rules'])} 条规则")
|
||||
return
|
||||
removed = bucket["rules"].pop(idx - 1)
|
||||
_save_data(config.RULES_PATH, DATA_CACHE)
|
||||
|
||||
gid = removed["group_id"] if removed["group_id"] is not None else "*"
|
||||
uid = removed["user_id"] if removed["user_id"] is not None else "*"
|
||||
kws = "、".join(removed["keywords"])
|
||||
await message.reply_text(f"✅ 已删除规则 {idx}:\n群={gid} 用户={uid} 关键词={kws}")
|
||||
|
||||
|
||||
async def cmd_list(client: Client, message) -> None:
|
||||
if not message.from_user or not _check_admin(message.from_user.id):
|
||||
return
|
||||
owner_id = message.from_user.id
|
||||
async with DATA_LOCK:
|
||||
bucket = _get_user_bucket(DATA_CACHE, owner_id)
|
||||
rules = list(bucket["rules"])
|
||||
notify_targets = bucket.get("notify_targets", [])
|
||||
|
||||
if not rules:
|
||||
await message.reply_text("当前没有任何规则。")
|
||||
return
|
||||
|
||||
lines = ["当前规则:"]
|
||||
for idx, rule in enumerate(rules, start=1):
|
||||
kws = "、".join(rule["keywords"]) if rule["keywords"] != ["*"] else "*"
|
||||
gid = rule["group_id"] if rule["group_id"] is not None else "*"
|
||||
uid = rule["user_id"] if rule["user_id"] is not None else "*"
|
||||
lines.append(f"{idx}. 群={gid} 用户={uid} 关键词={kws}")
|
||||
notify_targets = bucket.get("notify_targets", [])
|
||||
if notify_targets:
|
||||
lines.append(f"通知目标:{', '.join(str(t) for t in notify_targets)}")
|
||||
else:
|
||||
lines.append("通知目标:未设置(默认发送给你)")
|
||||
await message.reply_text("\n".join(lines))
|
||||
|
||||
|
||||
async def cmd_notify(client: Client, message) -> None:
|
||||
if not message.from_user or not _check_admin(message.from_user.id):
|
||||
return
|
||||
args = message.text.split()
|
||||
if len(args) < 2:
|
||||
await message.reply_text("用法:\n/notify add 目标ID - 添加通知目标\n/notify del 目标ID - 删除通知目标\n/notify list - 查看所有通知目标\n/notify clear - 清空所有通知目标")
|
||||
return
|
||||
|
||||
action = args[1].lower()
|
||||
owner_id = message.from_user.id
|
||||
|
||||
if action == "list":
|
||||
async with DATA_LOCK:
|
||||
bucket = _get_user_bucket(DATA_CACHE, owner_id)
|
||||
targets = bucket.get("notify_targets", [])
|
||||
if not targets:
|
||||
await message.reply_text("当前没有设置通知目标(默认发送给你)")
|
||||
else:
|
||||
lines = ["📌 当前通知目标:"]
|
||||
for i, t in enumerate(targets, 1):
|
||||
lines.append(f" {i}. {t}")
|
||||
await message.reply_text("\n".join(lines))
|
||||
return
|
||||
|
||||
if action == "clear":
|
||||
async with DATA_LOCK:
|
||||
bucket = _get_user_bucket(DATA_CACHE, owner_id)
|
||||
bucket["notify_targets"] = []
|
||||
_save_data(config.RULES_PATH, DATA_CACHE)
|
||||
await message.reply_text("✅ 已清空所有通知目标。")
|
||||
return
|
||||
|
||||
if action in ("add", "del") and len(args) < 3:
|
||||
await message.reply_text(f"用法:/notify {action} 目标ID")
|
||||
return
|
||||
|
||||
if action == "add":
|
||||
try:
|
||||
target_id = int(args[2])
|
||||
except ValueError:
|
||||
await message.reply_text("目标ID 必须是数字。")
|
||||
return
|
||||
async with DATA_LOCK:
|
||||
bucket = _get_user_bucket(DATA_CACHE, owner_id)
|
||||
if target_id in bucket["notify_targets"]:
|
||||
await message.reply_text("该目标已存在。")
|
||||
return
|
||||
bucket["notify_targets"].append(target_id)
|
||||
_save_data(config.RULES_PATH, DATA_CACHE)
|
||||
await message.reply_text(f"✅ 已添加通知目标:{target_id}")
|
||||
|
||||
elif action == "del":
|
||||
try:
|
||||
target_id = int(args[2])
|
||||
except ValueError:
|
||||
await message.reply_text("目标ID 必须是数字。")
|
||||
return
|
||||
async with DATA_LOCK:
|
||||
bucket = _get_user_bucket(DATA_CACHE, owner_id)
|
||||
if target_id not in bucket["notify_targets"]:
|
||||
await message.reply_text("该目标不存在。")
|
||||
return
|
||||
bucket["notify_targets"].remove(target_id)
|
||||
_save_data(config.RULES_PATH, DATA_CACHE)
|
||||
await message.reply_text(f"✅ 已删除通知目标:{target_id}")
|
||||
|
||||
else:
|
||||
# 兼容旧用法:/notify 目标ID 直接添加
|
||||
try:
|
||||
target_id = int(args[1])
|
||||
except ValueError:
|
||||
await message.reply_text("未知操作,请使用 add/del/list/clear")
|
||||
return
|
||||
async with DATA_LOCK:
|
||||
bucket = _get_user_bucket(DATA_CACHE, owner_id)
|
||||
if target_id not in bucket["notify_targets"]:
|
||||
bucket["notify_targets"].append(target_id)
|
||||
_save_data(config.RULES_PATH, DATA_CACHE)
|
||||
await message.reply_text(f"✅ 已添加通知目标:{target_id}")
|
||||
|
||||
|
||||
async def cmd_admin(client: Client, message) -> None:
|
||||
if not message.from_user or not _is_super_admin(message.from_user.id):
|
||||
return
|
||||
|
||||
args = message.text.split()
|
||||
if len(args) < 2:
|
||||
await message.reply_text("用法:/admin add|del|list [用户ID]")
|
||||
return
|
||||
|
||||
action = args[1].lower()
|
||||
global ADMINS_CACHE
|
||||
|
||||
if action == "list":
|
||||
super_admins = config.SUPER_ADMIN_IDS
|
||||
dynamic_admins = ADMINS_CACHE
|
||||
lines = ["👑 超级管理员:"]
|
||||
lines.extend([f" • {uid}" for uid in super_admins] or [" (无)"])
|
||||
lines.append("👤 普通管理员:")
|
||||
lines.extend([f" • {uid}" for uid in dynamic_admins] or [" (无)"])
|
||||
await message.reply_text("\n".join(lines))
|
||||
return
|
||||
|
||||
if len(args) < 3:
|
||||
await message.reply_text("请提供用户ID")
|
||||
return
|
||||
|
||||
try:
|
||||
target_id = int(args[2])
|
||||
except ValueError:
|
||||
await message.reply_text("用户ID 必须是数字")
|
||||
return
|
||||
|
||||
if action == "add":
|
||||
if target_id in config.SUPER_ADMIN_IDS:
|
||||
await message.reply_text("该用户已是超级管理员")
|
||||
return
|
||||
if target_id in ADMINS_CACHE:
|
||||
await message.reply_text("该用户已是管理员")
|
||||
return
|
||||
ADMINS_CACHE.append(target_id)
|
||||
_save_admins(ADMINS_CACHE)
|
||||
await message.reply_text(f"✅ 已添加管理员:{target_id}")
|
||||
elif action == "del":
|
||||
if target_id in config.SUPER_ADMIN_IDS:
|
||||
await message.reply_text("无法删除超级管理员")
|
||||
return
|
||||
if target_id not in ADMINS_CACHE:
|
||||
await message.reply_text("该用户不是管理员")
|
||||
return
|
||||
ADMINS_CACHE.remove(target_id)
|
||||
_save_admins(ADMINS_CACHE)
|
||||
await message.reply_text(f"✅ 已删除管理员:{target_id}")
|
||||
else:
|
||||
await message.reply_text("未知操作,请使用 add/del/list")
|
||||
|
||||
|
||||
async def cmd_help(client: Client, message) -> None:
|
||||
if not message.from_user or not _check_admin(message.from_user.id):
|
||||
return
|
||||
|
||||
help_text = """📖 使用帮助
|
||||
|
||||
🔍 监听管理:
|
||||
/watch 群ID|* 用户ID|* 关键词|*
|
||||
添加监听规则(* 表示匹配所有)
|
||||
/unwatch 序号
|
||||
删除监听规则(序号从 /list 查看)
|
||||
/list
|
||||
查看所有规则
|
||||
|
||||
📌 示例:
|
||||
/watch * 123456 * - 监控用户在所有群的所有消息
|
||||
/watch -100123 * 出售 - 监控某群所有人说"出售"
|
||||
/watch -100123 123456 三折 - 精确监控
|
||||
|
||||
🔔 通知设置:
|
||||
/notify add 目标ID - 添加通知目标
|
||||
/notify del 目标ID - 删除通知目标
|
||||
/notify list - 查看所有通知目标
|
||||
/notify clear - 清空所有通知目标
|
||||
|
||||
👑 管理员(仅超管):
|
||||
/admin add 用户ID - 添加管理员
|
||||
/admin del 用户ID - 删除管理员
|
||||
/admin list - 查看管理员列表
|
||||
|
||||
💡 提示:
|
||||
• 群ID 通常是负数,如 -1001234567
|
||||
• 用户ID 可通过 @userinfobot 获取"""
|
||||
|
||||
await message.reply_text(help_text)
|
||||
|
||||
|
||||
async def process_message(message) -> None:
|
||||
if not message.from_user or not message.chat:
|
||||
return
|
||||
|
||||
content = message.text or message.caption
|
||||
if not content:
|
||||
return
|
||||
|
||||
group_id = message.chat.id
|
||||
sender_id = message.from_user.id
|
||||
msg_id = message.id
|
||||
|
||||
if not _remember_message(group_id, msg_id):
|
||||
return
|
||||
|
||||
content_lower = content.lower()
|
||||
|
||||
async with DATA_LOCK:
|
||||
data_snapshot = json.loads(json.dumps(DATA_CACHE))
|
||||
|
||||
matched: Dict[str, Dict[str, Any]] = {}
|
||||
for owner_id, bucket in data_snapshot.get("users", {}).items():
|
||||
for rule in bucket.get("rules", []):
|
||||
rule_group = rule.get("group_id")
|
||||
rule_user = rule.get("user_id")
|
||||
|
||||
if rule_group is not None and rule_group != group_id:
|
||||
continue
|
||||
if rule_user is not None and rule_user != sender_id:
|
||||
continue
|
||||
|
||||
keywords = rule.get("keywords", [])
|
||||
if keywords == ["*"]:
|
||||
hit = ["*"]
|
||||
else:
|
||||
hit = [kw for kw in keywords if _keyword_hit(content_lower, kw)]
|
||||
if not hit:
|
||||
continue
|
||||
|
||||
entry = matched.setdefault(owner_id, {"keywords": set(), "notify_targets": bucket.get("notify_targets", [])})
|
||||
entry["keywords"].update(hit)
|
||||
|
||||
if not matched or bot_client is None:
|
||||
return
|
||||
|
||||
group_name = message.chat.title or message.chat.username or str(group_id)
|
||||
username = message.from_user.username
|
||||
display_name = (message.from_user.first_name or "")
|
||||
if message.from_user.last_name:
|
||||
display_name = f"{display_name} {message.from_user.last_name}".strip()
|
||||
if username:
|
||||
display_name = f"{display_name} (@{username})".strip()
|
||||
if not display_name:
|
||||
display_name = str(sender_id)
|
||||
|
||||
chat_username = message.chat.username
|
||||
if chat_username:
|
||||
group_link = f"https://t.me/{chat_username}"
|
||||
msg_link = f"https://t.me/{chat_username}/{msg_id}"
|
||||
else:
|
||||
chat_id_str = str(group_id).replace("-100", "")
|
||||
group_link = f"https://t.me/c/{chat_id_str}"
|
||||
msg_link = f"https://t.me/c/{chat_id_str}/{msg_id}"
|
||||
|
||||
for owner_id, info in matched.items():
|
||||
keywords_raw = "、".join(sorted(info["keywords"]))
|
||||
keywords = "全部" if keywords_raw == "*" else keywords_raw
|
||||
notify_targets = info.get("notify_targets", [])
|
||||
if not notify_targets:
|
||||
notify_targets = [int(owner_id)] # 默认发给自己
|
||||
|
||||
text = (
|
||||
"🔔 消息提醒\n\n"
|
||||
f"👥 群:{group_name}\n"
|
||||
f"👤 用户:{display_name}\n"
|
||||
f"🆔 ID:{sender_id}\n"
|
||||
f"🔑 关键词:{keywords}\n"
|
||||
f"💬 消息:{content}\n"
|
||||
f"📍 直达:{msg_link}"
|
||||
)
|
||||
|
||||
for notify_target in notify_targets:
|
||||
try:
|
||||
await bot_client.send_message(notify_target, text)
|
||||
print(f"[通知] 已发送通知到 {notify_target}")
|
||||
except RPCError as exc:
|
||||
print(f"[错误] 发送通知到 {notify_target} 失败: {exc}")
|
||||
|
||||
|
||||
async def on_user_message(client: Client, message) -> None:
|
||||
await process_message(message)
|
||||
|
||||
|
||||
async def poll_dialogs() -> None:
|
||||
global user_client
|
||||
if user_client is None:
|
||||
return
|
||||
|
||||
async with DATA_LOCK:
|
||||
data_snapshot = json.loads(json.dumps(DATA_CACHE))
|
||||
|
||||
chat_ids: Set[int] = set()
|
||||
for bucket in data_snapshot.get("users", {}).values():
|
||||
for rule in bucket.get("rules", []):
|
||||
gid = rule.get("group_id")
|
||||
if gid is not None:
|
||||
chat_ids.add(gid)
|
||||
|
||||
if not chat_ids:
|
||||
return
|
||||
|
||||
print(f"[轮询] 检查 {len(chat_ids)} 个群...")
|
||||
|
||||
for chat_id in chat_ids:
|
||||
try:
|
||||
messages = [msg async for msg in user_client.get_chat_history(chat_id, limit=5)]
|
||||
for msg in reversed(messages):
|
||||
if msg:
|
||||
await process_message(msg)
|
||||
except Exception:
|
||||
print(f"[警告] 群 {chat_id} 获取失败")
|
||||
|
||||
print("[轮询] 检查完成")
|
||||
|
||||
|
||||
async def polling_loop() -> None:
|
||||
while True:
|
||||
try:
|
||||
await poll_dialogs()
|
||||
except Exception as exc:
|
||||
print(f"[错误] 轮询循环异常: {exc}")
|
||||
await asyncio.sleep(POLL_INTERVAL_SECONDS)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
if config.API_ID == 0 or not config.API_HASH:
|
||||
raise SystemExit("缺少 TG_API_ID 或 TG_API_HASH 环境变量。")
|
||||
if not config.BOT_TOKEN:
|
||||
raise SystemExit("缺少 TG_BOT_TOKEN 环境变量。")
|
||||
if not config.USER_SESSION_STRING:
|
||||
raise SystemExit("缺少 TG_USER_SESSION_STRING 环境变量。")
|
||||
|
||||
global DATA_CACHE, ADMINS_CACHE, bot_client, user_client
|
||||
DATA_CACHE = _load_data(config.RULES_PATH)
|
||||
ADMINS_CACHE = _load_admins()
|
||||
|
||||
bot = Client(
|
||||
name="bot",
|
||||
api_id=config.API_ID,
|
||||
api_hash=config.API_HASH,
|
||||
bot_token=config.BOT_TOKEN,
|
||||
workdir="./",
|
||||
)
|
||||
|
||||
user = Client(
|
||||
name="user",
|
||||
api_id=config.API_ID,
|
||||
api_hash=config.API_HASH,
|
||||
session_string=config.USER_SESSION_STRING,
|
||||
workdir="./",
|
||||
)
|
||||
|
||||
bot.add_handler(MessageHandler(cmd_watch, filters.command("watch")))
|
||||
bot.add_handler(MessageHandler(cmd_unwatch, filters.command("unwatch")))
|
||||
bot.add_handler(MessageHandler(cmd_list, filters.command("list")))
|
||||
bot.add_handler(MessageHandler(cmd_notify, filters.command("notify")))
|
||||
bot.add_handler(MessageHandler(cmd_admin, filters.command("admin")))
|
||||
bot.add_handler(MessageHandler(cmd_help, filters.command("help")))
|
||||
|
||||
user.add_handler(MessageHandler(on_user_message, filters.incoming))
|
||||
|
||||
bot_client = bot
|
||||
user_client = user
|
||||
|
||||
await bot.start()
|
||||
await user.start()
|
||||
|
||||
print("Bot 和 Userbot 已启动。")
|
||||
print(f"使用轮询模式监听消息(每{POLL_INTERVAL_SECONDS}秒检查一次)...")
|
||||
|
||||
polling_task = asyncio.create_task(polling_loop())
|
||||
|
||||
await idle()
|
||||
|
||||
polling_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await polling_task
|
||||
await bot.stop()
|
||||
await user.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyrogram==2.0.106
|
||||
tgcrypto==1.2.5
|
||||
14
rules.json
Normal file
14
rules.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"users": {
|
||||
"YOUR_TG_ID": {
|
||||
"rules": [
|
||||
{
|
||||
"group_id": -100123456789,
|
||||
"user_id": 123456789,
|
||||
"keywords": ["*"]
|
||||
}
|
||||
],
|
||||
"notify_targets": [-100987654321]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user