Bot become a Community Guard & Post send manager

added: dictionary support for censore
message/user management with dict triggers
This commit is contained in:
2025-08-22 21:44:14 +09:00
parent efdafb0efa
commit c16ec54891
27 changed files with 1746 additions and 184 deletions

View File

@@ -0,0 +1,152 @@
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