Bot become a Community Guard & Post send manager
added: dictionary support for censore message/user management with dict triggers
This commit is contained in:
152
app/bot/handlers/moderation.py
Normal file
152
app/bot/handlers/moderation.py
Normal 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
|
||||
Reference in New Issue
Block a user