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