added: dictionary support for censore message/user management with dict triggers
153 lines
6.5 KiB
Python
153 lines
6.5 KiB
Python
import asyncio, re
|
|
from datetime import datetime, timedelta
|
|
from telegram import Update, ChatPermissions
|
|
from telegram.constants import ChatType, ParseMode
|
|
from telegram.ext import ContextTypes
|
|
|
|
from app.db.session import get_session
|
|
from app.db.models import MessageEvent, ModerationLog
|
|
from app.moderation.engine import (
|
|
get_policy_for_chat, compute_content_hash, redis_rate_check, redis_dupe_check,
|
|
add_strike_and_decide_action, _domain_sets, dict_cache, _compile_dicts
|
|
)
|
|
from app.infra.metrics import MSG_PROCESSED, MSG_BLOCKED, MOD_LAT
|
|
|
|
WARN_TTL = 20
|
|
|
|
async def _autodel(ctx: ContextTypes.DEFAULT_TYPE, chat_id: int, mid: int, ttl: int = WARN_TTL):
|
|
try:
|
|
await asyncio.sleep(ttl)
|
|
await ctx.bot.delete_message(chat_id=chat_id, message_id=mid)
|
|
except Exception:
|
|
pass
|
|
|
|
def _extract_media_ids(msg) -> list[str]:
|
|
if msg.photo: return [msg.photo[-1].file_id]
|
|
if msg.video: return [msg.video.file_id]
|
|
if msg.animation: return [msg.animation.file_id]
|
|
if msg.document: return [msg.document.file_id]
|
|
return []
|
|
|
|
async def moderate_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
|
msg = update.effective_message
|
|
chat = update.effective_chat
|
|
user = update.effective_user
|
|
if chat.type not in (ChatType.GROUP, ChatType.SUPERGROUP):
|
|
return
|
|
if not user or user.is_bot:
|
|
return
|
|
|
|
MSG_PROCESSED.inc()
|
|
with MOD_LAT.time():
|
|
text = (getattr(msg, "text", None) or getattr(msg, "caption", None) or "")
|
|
media_ids = _extract_media_ids(msg)
|
|
h = compute_content_hash(text, media_ids)
|
|
reasons = []
|
|
|
|
with get_session() as s:
|
|
policy = get_policy_for_chat(s, chat.id)
|
|
if not policy:
|
|
return
|
|
|
|
# лог события для аналитики
|
|
s.add(MessageEvent(chat_id=chat.id, tg_user_id=user.id, message_id=msg.message_id, content_hash=h))
|
|
s.commit()
|
|
|
|
# rate-limit и дубликаты через Redis (если есть)
|
|
if policy.user_msg_per_minute > 0:
|
|
ok = await redis_rate_check(chat.id, user.id, policy.user_msg_per_minute)
|
|
if not ok:
|
|
reasons.append(f"ratelimit>{policy.user_msg_per_minute}/min")
|
|
if policy.duplicate_window_seconds > 0:
|
|
ok2 = await redis_dupe_check(chat.id, user.id, h, policy.duplicate_window_seconds)
|
|
if not ok2:
|
|
reasons.append("duplicate")
|
|
|
|
# дешёвые проверки
|
|
URL_RE = re.compile(r'https?://[^\s)]+', re.IGNORECASE)
|
|
MENTION_RE = re.compile(r'@\w+', re.UNICODE)
|
|
links = URL_RE.findall(text or "")
|
|
if policy.max_links >= 0 and len(links) > policy.max_links:
|
|
reasons.append(f"links>{policy.max_links}")
|
|
mentions = MENTION_RE.findall(text or "")
|
|
if policy.max_mentions >= 0 and len(mentions) > policy.max_mentions:
|
|
reasons.append(f"mentions>{policy.max_mentions}")
|
|
|
|
wl, bl = _domain_sets(s, policy)
|
|
from app.moderation.engine import _find_domains
|
|
doms = _find_domains(text or "")
|
|
if policy.use_whitelist and wl:
|
|
bad = [d for d in doms if d not in wl]
|
|
if bad:
|
|
reasons.append("not_whitelisted:" + ",".join(sorted(set(bad))))
|
|
else:
|
|
bad = [d for d in doms if d in bl]
|
|
if bad:
|
|
reasons.append("blacklisted:" + ",".join(sorted(set(bad))))
|
|
|
|
# словари (только если дешёвые проверки не сработали)
|
|
if not reasons:
|
|
ac, reg = dict_cache.get(policy.id) or _compile_dicts(s, policy)
|
|
lo = (text or "").lower()
|
|
matched = False
|
|
if ac is not None:
|
|
for _, _ in ac.iter(lo):
|
|
matched = True
|
|
break
|
|
if not matched:
|
|
for r in reg:
|
|
if r.search(text or ""):
|
|
matched = True
|
|
break
|
|
if matched:
|
|
reasons.append("dictionary_match")
|
|
|
|
if not reasons:
|
|
return
|
|
|
|
action = add_strike_and_decide_action(s, policy, chat.id, user.id)
|
|
|
|
# Применение наказания
|
|
MSG_BLOCKED.inc()
|
|
performed = "none"
|
|
try:
|
|
if action in ("delete", "warn", "timeout", "ban"):
|
|
try:
|
|
await ctx.bot.delete_message(chat_id=chat.id, message_id=msg.message_id)
|
|
except Exception:
|
|
pass
|
|
if action == "warn":
|
|
warn = await msg.reply_text(
|
|
f"⚠️ Сообщение удалено по правилам чата.\nПричины: {', '.join(reasons)}",
|
|
parse_mode=ParseMode.HTML
|
|
)
|
|
ctx.application.create_task(_autodel(ctx, chat.id, warn.message_id))
|
|
performed = "warn"
|
|
elif action == "timeout":
|
|
until = datetime.utcnow() + timedelta(minutes=policy.timeout_minutes)
|
|
await ctx.bot.restrict_chat_member(chat_id=chat.id, user_id=user.id,
|
|
permissions=ChatPermissions(can_send_messages=False),
|
|
until_date=until)
|
|
info = await msg.reply_text(
|
|
f"⏳ Пользователь ограничен на {policy.timeout_minutes} минут. Причины: {', '.join(reasons)}"
|
|
)
|
|
ctx.application.create_task(_autodel(ctx, chat.id, info.message_id))
|
|
performed = "timeout"
|
|
elif action == "ban":
|
|
await ctx.bot.ban_chat_member(chat_id=chat.id, user_id=user.id)
|
|
info = await msg.reply_text(f"🚫 Пользователь забанен. Причины: {', '.join(reasons)}")
|
|
ctx.application.create_task(_autodel(ctx, chat.id, info.message_id))
|
|
performed = "ban"
|
|
elif action == "delete":
|
|
performed = "delete"
|
|
finally:
|
|
# Лог модерации
|
|
try:
|
|
with get_session() as s2:
|
|
s2.add(ModerationLog(chat_id=chat.id, tg_user_id=user.id,
|
|
message_id=msg.message_id,
|
|
reason="; ".join(reasons), action=performed))
|
|
s2.commit()
|
|
except Exception:
|
|
pass
|