soome changes

This commit is contained in:
2025-08-28 04:28:37 +09:00
parent 8b554f5968
commit 00b00f5bf3
9 changed files with 378 additions and 216 deletions

View File

@@ -0,0 +1,249 @@
# app/bot/handlers/dict_commands.py
from telegram import Update
from telegram.constants import ChatType
from telegram.ext import ContextTypes
from sqlalchemy import func
from app.db.session import get_session
from app.db.models import (
User, SecurityPolicy, ChatSecurity,
SpamDictionary, PolicyDictionaryLink, DictionaryEntry,
)
ALLOWED_CATEGORIES = {"spam", "scam", "adult", "profanity", "custom"}
def _get_or_create_policy(session, owner_tg_id: int) -> SecurityPolicy:
u = session.query(User).filter_by(tg_id=owner_tg_id).first()
if not u:
u = User(tg_id=owner_tg_id, name="")
session.add(u); session.commit(); session.refresh(u)
p = session.query(SecurityPolicy).filter_by(owner_user_id=u.id, name="Balanced").first()
if p:
return p
p = SecurityPolicy(owner_user_id=u.id, name="Balanced")
session.add(p); session.commit(); session.refresh(p)
return p
async def _is_group_admin(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> bool:
"""В группе/канале разрешаем только администраторам."""
chat = update.effective_chat
if chat.type in (ChatType.GROUP, ChatType.SUPERGROUP, ChatType.CHANNEL):
try:
m = await ctx.bot.get_chat_member(chat.id, update.effective_user.id)
return m.status in ("administrator", "creator")
except Exception:
return False
return True
def _resolve_policy_and_owner(session, update: Update) -> tuple[SecurityPolicy, User, str]:
"""
В группе/канале: берём политику, привязанную к чату.
В ЛС: дефолтную политику Balanced владельца.
"""
chat = update.effective_chat
user = session.query(User).filter_by(tg_id=update.effective_user.id).first()
if not user:
user = User(tg_id=update.effective_user.id, name=update.effective_user.full_name or "")
session.add(user); session.commit(); session.refresh(user)
if chat.type in (ChatType.GROUP, ChatType.SUPERGROUP, ChatType.CHANNEL):
cs = session.query(ChatSecurity).filter_by(chat_id=chat.id).first()
if not cs:
raise RuntimeError("not_bound")
p = session.get(SecurityPolicy, cs.policy_id)
if not p:
raise RuntimeError("policy_missing")
return p, user, f"политика чата «{chat.title or chat.id}»"
else:
p = _get_or_create_policy(session, update.effective_user.id)
return p, user, "ваша политика Balanced (ЛС)"
def _find_dict(session, owner: User, token: str) -> SpamDictionary | None:
"""token — numeric id или точное имя словаря владельца (без глобальных)."""
token = (token or "").strip()
if not token:
return None
if token.isdigit():
d = session.get(SpamDictionary, int(token))
return d if d and d.owner_user_id == owner.id else None
return (
session.query(SpamDictionary)
.filter(SpamDictionary.owner_user_id == owner.id, SpamDictionary.name == token)
.first()
)
# ===================== Команды =====================
async def dicts_link_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await _is_group_admin(update, ctx):
return
args = ctx.args or []
if not args:
await update.effective_message.reply_text("Использование: /dicts_link <id|имя_словаря>")
return
token = " ".join(args).strip()
with get_session() as s:
try:
p, owner, scope = _resolve_policy_and_owner(s, update)
except RuntimeError as e:
if str(e) == "not_bound":
await update.effective_message.reply_text("Политика не привязана. В группе откройте /security → «Привязать к этому чату».")
return
await update.effective_message.reply_text("Политика недоступна. Привяжите её через /security.")
return
d = _find_dict(s, owner, token)
if not d:
await update.effective_message.reply_text(f"Словарь не найден среди ваших: «{token}». Проверьте id/имя (см. /dicts).")
return
link = s.query(PolicyDictionaryLink).filter_by(policy_id=p.id, dictionary_id=d.id).first()
if link:
await update.effective_message.reply_text(f"Уже привязан: «{d.name}» ({scope}).")
return
s.add(PolicyDictionaryLink(policy_id=p.id, dictionary_id=d.id)); s.commit();
from app.moderation.engine import dict_cache
dict_cache.invalidate(p.id)
rules = s.query(DictionaryEntry).filter_by(dictionary_id=d.id).count()
await update.effective_message.reply_text(f"Привязал «{d.name}» ({d.category}/{d.kind}, правил: {rules}) к {scope}.")
async def dicts_unlink_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await _is_group_admin(update, ctx):
return
args = ctx.args or []
if not args:
await update.effective_message.reply_text("Использование: /dicts_unlink <id|имя_словаря>")
return
token = " ".join(args).strip()
with get_session() as s:
try:
p, owner, scope = _resolve_policy_and_owner(s, update)
except RuntimeError:
await update.effective_message.reply_text("Политика не привязана. В группе откройте /security → «Привязать к этому чату».")
return
d = _find_dict(s, owner, token)
if not d:
await update.effective_message.reply_text(f"Словарь не найден: «{token}».")
return
link = s.query(PolicyDictionaryLink).filter_by(policy_id=p.id, dictionary_id=d.id).first()
if not link:
await update.effective_message.reply_text(f"Словарь «{d.name}» и так не привязан к {scope}.")
return
s.delete(link); s.commit()
from app.moderation.engine import dict_cache
dict_cache.invalidate(p.id)
await update.effective_message.reply_text(f"Отвязал «{d.name}» от {scope}.")
async def dicts_link_all_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await _is_group_admin(update, ctx):
return
args = ctx.args or []
cat = (args[0].lower() if args else None)
if cat and cat not in ALLOWED_CATEGORIES:
await update.effective_message.reply_text(f"Категория неизвестна. Используйте: {', '.join(sorted(ALLOWED_CATEGORIES))} (или без аргумента).")
return
with get_session() as s:
try:
p, owner, scope = _resolve_policy_and_owner(s, update)
except RuntimeError:
await update.effective_message.reply_text("Политика не привязана. В группе откройте /security → «Привязать к этому чату».")
return
q = s.query(SpamDictionary).filter_by(owner_user_id=owner.id)
if cat:
q = q.filter(SpamDictionary.category == cat)
ds = q.all()
if not ds:
await update.effective_message.reply_text("У вас нет словарей для привязки (см. /spam_import).")
return
existing = {x.dictionary_id for x in s.query(PolicyDictionaryLink).filter_by(policy_id=p.id).all()}
added = 0
for d in ds:
if d.id in existing:
continue
s.add(PolicyDictionaryLink(policy_id=p.id, dictionary_id=d.id)); added += 1
s.commit()
from app.moderation.engine import dict_cache
dict_cache.invalidate(p.id)
await update.effective_message.reply_text(f"Привязал {added} словарей к {scope}." + (f" (категория: {cat})" if cat else ""))
async def dicts_unlink_all_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await _is_group_admin(update, ctx):
return
args = ctx.args or []
cat = (args[0].lower() if args else None)
if cat and cat not in ALLOWED_CATEGORIES:
await update.effective_message.reply_text(f"Категория неизвестна. Используйте: {', '.join(sorted(ALLOWED_CATEGORIES))} (или без аргумента).")
return
with get_session() as s:
try:
p, owner, scope = _resolve_policy_and_owner(s, update)
except RuntimeError:
await update.effective_message.reply_text("Политика не привязана. В группе откройте /security → «Привязать к этому чату».")
return
q = (
s.query(SpamDictionary)
.join(PolicyDictionaryLink, PolicyDictionaryLink.dictionary_id == SpamDictionary.id)
.filter(PolicyDictionaryLink.policy_id == p.id, SpamDictionary.owner_user_id == owner.id)
)
if cat:
q = q.filter(SpamDictionary.category == cat)
ds = q.all()
if not ds:
await update.effective_message.reply_text("Нет привязанных словарей для отвязки.")
return
deleted = 0
for d in ds:
link = s.query(PolicyDictionaryLink).filter_by(policy_id=p.id, dictionary_id=d.id).first()
if link:
s.delete(link); deleted += 1
s.commit()
from app.moderation.engine import dict_cache
dict_cache.invalidate(p.id)
await update.effective_message.reply_text(f"Отвязал {deleted} словарей от {scope}." + (f" (категория: {cat})" if cat else ""))
async def dicts_linked_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if not await _is_group_admin(update, ctx):
return
with get_session() as s:
try:
p, owner, scope = _resolve_policy_and_owner(s, update)
except RuntimeError:
await update.effective_message.reply_text("Политика не привязана. В группе откройте /security → «Привязать к этому чату».")
return
links = s.query(PolicyDictionaryLink).filter_by(policy_id=p.id).all()
if not links:
await update.effective_message.reply_text(f"К {scope} пока ничего не привязано.")
return
d_ids = [lnk.dictionary_id for lnk in links]
dicts = s.query(SpamDictionary).filter(SpamDictionary.id.in_(d_ids)).all()
counts = {
d.id: s.query(func.count(DictionaryEntry.id)).filter(DictionaryEntry.dictionary_id == d.id).scalar() or 0
for d in dicts
}
lines = [f"Привязано к {scope}:"]
for d in dicts:
lines.append(f"• [{d.id}] {d.name} ({d.category}/{d.kind}) — правил: {counts.get(d.id, 0)}")
await update.effective_message.reply_text("\n".join(lines))

View File

@@ -11,7 +11,7 @@ from app.db.models import (
SpamDictionary, DictionaryEntry, PolicyDictionaryLink,
)
from app.bot.keyboards.security import kb_policy
from app.moderation.engine import dict_cache
# ---------- helpers ----------
def _get_or_create_policy(session, owner_tg_id: int) -> SecurityPolicy:
@@ -54,7 +54,9 @@ def _import_entries(session, owner_tg_id: int, params: dict, entries: list[str])
u = session.query(User).filter_by(tg_id=owner_tg_id).first()
if not u:
u = User(tg_id=owner_tg_id, name="")
session.add(u); session.commit(); session.refresh(u)
session.add(u)
session.commit()
session.refresh(u)
# создать словарь
d = SpamDictionary(
@@ -139,6 +141,7 @@ async def security_cb(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
field = parts[2]
setattr(p, field, not getattr(p, field))
s.commit()
dict_cache.invalidate(cs.policy_id)
elif action == "adj":
field, delta = parts[2], int(parts[3])
@@ -259,9 +262,9 @@ async def dicts_cb(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
return
link = s.query(PolicyDictionaryLink).filter_by(policy_id=policy_id, dictionary_id=dict_id).first()
if link:
s.delete(link); s.commit()
s.delete(link); s.commit(); dict_cache.invalidate(policy_id)
else:
s.add(PolicyDictionaryLink(policy_id=policy_id, dictionary_id=dict_id)); s.commit()
s.add(PolicyDictionaryLink(policy_id=policy_id, dictionary_id=dict_id)); s.commit(); dict_cache.invalidate(policy_id)
u = s.query(User).filter_by(tg_id=update.effective_user.id).first()
dicts = s.query(SpamDictionary).filter_by(owner_user_id=u.id).order_by(SpamDictionary.created_at.desc()).all()

View File

@@ -4,10 +4,10 @@ from app.db.models import SecurityPolicy
def kb_policy(p: SecurityPolicy, chat_bound: bool = False, enabled: bool = False):
def onoff(b): return "" if b else ""
rows = [
[InlineKeyboardButton(f"Adult {onoff(p.block_adult)}", callback_data=f"pol:toggle:block_adult:{p.id}"),
InlineKeyboardButton(f"Spam {onoff(p.block_spam)}", callback_data=f"pol:toggle:block_spam:{p.id}")],
[InlineKeyboardButton(f"Scam {onoff(p.block_scam)}", callback_data=f"pol:toggle:block_scam:{p.id}"),
InlineKeyboardButton(f"Profanity {onoff(p.block_profanity)}", callback_data=f"pol:toggle:block_profanity:{p.id}")],
[InlineKeyboardButton(f"18+ {onoff(p.block_adult)}", callback_data=f"pol:toggle:block_adult:{p.id}"),
InlineKeyboardButton(f"Спам {onoff(p.block_spam)}", callback_data=f"pol:toggle:block_spam:{p.id}")],
[InlineKeyboardButton(f"Скам {onoff(p.block_scam)}", callback_data=f"pol:toggle:block_scam:{p.id}"),
InlineKeyboardButton(f"Брань {onoff(p.block_profanity)}", callback_data=f"pol:toggle:block_profanity:{p.id}")],
[InlineKeyboardButton(f"Cooldown {p.cooldown_seconds}s (-5)", callback_data=f"pol:adj:cooldown_seconds:-5:{p.id}"),
InlineKeyboardButton("(+5)", callback_data=f"pol:adj:cooldown_seconds:+5:{p.id}")],
[InlineKeyboardButton(f"Dupe {p.duplicate_window_seconds}s (-30)", callback_data=f"pol:adj:duplicate_window_seconds:-30:{p.id}"),

View File

View File

@@ -0,0 +1,18 @@
# app/bot/routers/channel.py
from telegram.ext import CommandHandler, filters
async def channel_redirect(update, ctx):
try:
await update.effective_message.reply_text(
"⚠️ В канале команды не поддерживаются. "
"Откройте ЛС со мной и выполните:\n"
"• /bind @username (или -100…)\n"
"• или /add_group и перешлите сюда пост из канала.",
disable_web_page_preview=True
)
except Exception:
pass
def register_channel_handlers(app):
# В канале любые «команды настроек» переводим в ЛС
app.add_handler(CommandHandler(["start","settings","security","dicts","spam_import"], channel_redirect, filters.ChatType.CHANNEL))

28
app/bot/routers/group.py Normal file
View File

@@ -0,0 +1,28 @@
# app/bot/routers/group.py
from telegram.ext import CommandHandler, MessageHandler, ChatMemberHandler, filters
from app.bot.handlers.join_info import on_my_chat_member
from app.bot.handlers.moderation import moderate_message
from app.bot.handlers.security import security_cmd, dicts_cmd
from app.bot.handlers.mod_status import mod_status_cmd
from app.bot.handlers.chat_id_cmd import chat_id_cmd
async def spam_import_redirect(update, ctx):
await update.effective_message.reply_text(
"Импорт словаря выполняется в ЛС. Откройте чат со мной и пришлите /spam_import.",
disable_web_page_preview=True
)
def register_group_handlers(app):
# Команды в группах/супергруппах
app.add_handler(CommandHandler("security", security_cmd, filters.ChatType.GROUPS))
app.add_handler(CommandHandler("dicts", dicts_cmd, filters.ChatType.GROUPS))
app.add_handler(CommandHandler("mod_status", mod_status_cmd, filters.ChatType.GROUPS))
app.add_handler(CommandHandler("id", chat_id_cmd, filters.ChatType.GROUPS))
# /spam_import — редирект в ЛС
app.add_handler(CommandHandler("spam_import", spam_import_redirect, filters.ChatType.GROUPS))
# Модерация всех сообщений (privacy mode должен быть Disabled)
app.add_handler(MessageHandler(filters.ChatType.GROUPS & ~filters.COMMAND, moderate_message))
# my_chat_member для уведомлений о добавлении бота
app.add_handler(ChatMemberHandler(on_my_chat_member, chat_member_types=ChatMemberHandler.MY_CHAT_MEMBER))

View File

@@ -0,0 +1,40 @@
# app/bot/routers/private.py
from telegram.ext import CommandHandler, MessageHandler, ChatMemberHandler, filters
from app.bot.handlers.start import start, help_cmd, groups_cmd
from app.bot.handlers.add_group import add_group_cmd, add_group_capture
from app.bot.handlers.drafts import new_cmd, on_text, on_text_gate
from app.bot.handlers.media import on_media
from app.bot.handlers.chat_id_cmd import chat_id_cmd
from app.bot.handlers.security import (
security_cmd, spam_import_cmd, spam_import_capture, spam_import_text_capture, dicts_cmd
)
from app.bot.handlers.cancel import cancel_cmd
from app.bot.handlers.debug import dbg_state_cmd
def register_private_handlers(app):
# Команды в ЛС
app.add_handler(CommandHandler("start", start, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("help", help_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("groups", groups_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("add_group", add_group_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("new", new_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("id", chat_id_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("security", security_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("dicts", dicts_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("spam_import", spam_import_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("cancel", cancel_cmd, filters.ChatType.PRIVATE))
app.add_handler(CommandHandler("dbg_state", dbg_state_cmd, filters.ChatType.PRIVATE))
# Импорт словаря — файл
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.Document.ALL, spam_import_capture))
# Импорт словаря — текст (не блокирует цепочку)
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND, spam_import_text_capture, block=False))
# Привязка чата пересланным сообщением
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.FORWARDED, add_group_capture))
# Медиа для черновика
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & (filters.PHOTO | filters.VIDEO | filters.ANIMATION), on_media))
# Приватный текст: шлюз (высший приоритет) → редактор
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND, on_text_gate, block=True))
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND, on_text))

View File

@@ -1,171 +1,30 @@
# import logging
# from telegram.ext import (
# ApplicationBuilder, CommandHandler, MessageHandler,
# CallbackQueryHandler, ChatMemberHandler, filters
# )
# from app.config import load_config
# from app.infra.metrics import start_metrics_server
# # базовые хэндлеры
# from app.bot.handlers.start import start, help_cmd, groups_cmd
# from app.bot.handlers.add_group import add_group_cmd, add_group_capture
# from app.bot.handlers.drafts import new_cmd, on_text
# from app.bot.handlers.media import on_media
# from app.bot.handlers.callbacks import on_callback
# from app.bot.handlers.join_info import on_my_chat_member
# from app.bot.handlers.chat_id_cmd import chat_id_cmd
# from app.bot.handlers.bind_chat import bind_chat_cb
# # безопасность/словарь
# from app.bot.handlers.security import (
# security_cmd, security_cb,
# spam_import_cmd, spam_import_capture, spam_import_text_capture,
# dicts_cmd, dicts_cb,
# )
# # модерация и диагностика
# from app.bot.handlers.moderation import moderate_message
# from app.bot.handlers.mod_status import mod_status_cmd # если нет — можете закомментировать
# from app.bot.handlers.errors import on_error
# from app.bot.handlers.cancel import cancel_cmd
# from app.bot.handlers.drafts import on_text_gate
# from app.bot.handlers.debug import dbg_state_cmd
# async def spam_import_redirect(update, ctx):
# await update.effective_message.reply_text(
# "Эту команду нужно выполнять в личке. "
# "Откройте чат со мной и пришлите /spam_import."
# )
# def main():
# cfg = load_config()
# logging.basicConfig(level=getattr(logging, cfg.log_level.upper(), logging.INFO))
# # ← СКРОЕМ «HTTP Request: …» от httpx/httpcore
# logging.getLogger("httpx").setLevel(logging.WARNING)
# logging.getLogger("httpx").propagate = False
# logging.getLogger("httpcore").setLevel(logging.WARNING)
# logging.getLogger("httpcore").propagate = False
# # (опционально) если лишний шум от urllib3/telegram:
# # logging.getLogger("urllib3").setLevel(logging.WARNING)
# # logging.getLogger("telegram").setLevel(logging.INFO)
# # запустить эндпоинт метрик (Prometheus будет ходить на порт cfg.metrics_port)
# start_metrics_server(cfg.metrics_port)
# app = ApplicationBuilder().token(cfg.bot_token).build()
# # --- Commands ---
# app.add_handler(CommandHandler("start", start))
# app.add_handler(CommandHandler("help", help_cmd))
# app.add_handler(CommandHandler("groups", groups_cmd))
# app.add_handler(CommandHandler("add_group", add_group_cmd))
# app.add_handler(CommandHandler("new", new_cmd))
# app.add_handler(CommandHandler("id", chat_id_cmd))
# app.add_handler(CommandHandler("mod_status", mod_status_cmd))
# app.add_handler(CommandHandler("cancel", cancel_cmd))
# app.add_handler(CommandHandler("dbg_state", dbg_state_cmd))
# # --- Callbacks (порядок важен) ---
# app.add_handler(CallbackQueryHandler(security_cb, pattern=r"^pol:")) # настройки защитника
# app.add_handler(CallbackQueryHandler(dicts_cb, pattern=r"^dict:")) # привязка словарей
# app.add_handler(CallbackQueryHandler(bind_chat_cb, pattern=r"^bind:")) # привязка канала кнопкой
# app.add_handler(CallbackQueryHandler(on_callback, pattern=r"^(draft_next_text:|tgl:|selall:|clear:|sendmulti:)"))
# # --- Private chat helpers ---
# app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.FORWARDED, add_group_capture))
# app.add_handler(MessageHandler(filters.ChatType.PRIVATE & (filters.PHOTO | filters.VIDEO | filters.ANIMATION), on_media))
# # --- Join/rights updates ---
# app.add_handler(ChatMemberHandler(on_my_chat_member, chat_member_types=ChatMemberHandler.MY_CHAT_MEMBER))
# # --- Security / Dict ---
# app.add_handler(CommandHandler("security", security_cmd))
# # /spam_import — ТОЛЬКО в ЛС
# app.add_handler(CommandHandler("spam_import", spam_import_cmd, filters.ChatType.PRIVATE))
# # редирект в группах
# app.add_handler(CommandHandler("spam_import", spam_import_redirect, filters.ChatType.GROUPS))
# # файл словаря
# app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.Document.ALL, spam_import_capture))
# # текст словаря/параметров — ДО редактора, и не блокируем цепочку
# app.add_handler(MessageHandler(
# filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND,
# spam_import_text_capture,
# block=False
# ))
# # обзор словарей для политики
# app.add_handler(CommandHandler("dicts", dicts_cmd))
# # --- Moderation in groups ---
# app.add_handler(MessageHandler(filters.ChatType.GROUPS & ~filters.COMMAND, moderate_message))
# app.add_handler(MessageHandler(
# filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND,
# on_text_gate,
# block=True
# ))
# # --- Draft editor (ПОСЛЕ импорт-хэндлеров) ---
# app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND, on_text))
# app.add_error_handler(on_error)
# app.run_polling(allowed_updates=None)
# if __name__ == "__main__":
# main()
# app/main.py
import logging
from telegram.ext import (
ApplicationBuilder, CommandHandler, MessageHandler,
CallbackQueryHandler, ChatMemberHandler, filters
)
from telegram.ext import ApplicationBuilder
from app.config import load_config
from app.infra.metrics import start_metrics_server
# базовые хэндлеры
from app.bot.handlers.start import start, help_cmd, groups_cmd
from app.bot.handlers.add_group import add_group_cmd, add_group_capture
from app.bot.handlers.drafts import new_cmd, on_text, on_text_gate # ← шлюз + редактор
from app.bot.handlers.media import on_media
# глобальные колбэки (узкие паттерны) и ошибки — держим в main
from app.bot.handlers.callbacks import on_callback
from app.bot.handlers.join_info import on_my_chat_member
from app.bot.handlers.chat_id_cmd import chat_id_cmd
from app.bot.handlers.bind_chat import bind_chat_cb
# безопасность / словари
from app.bot.handlers.security import (
security_cmd, security_cb,
spam_import_cmd, spam_import_capture, spam_import_text_capture,
dicts_cmd, dicts_cb,
from app.bot.handlers.security import security_cb
from app.bot.handlers.dict_commands import (
dicts_link_cmd, dicts_unlink_cmd,
dicts_link_all_cmd, dicts_unlink_all_cmd,
dicts_linked_cmd,
)
# модерация и диагностика
from app.bot.handlers.moderation import moderate_message
from app.bot.handlers.mod_status import mod_status_cmd # если нет — закомментировать
from app.bot.handlers.errors import on_error
from app.bot.handlers.cancel import cancel_cmd
from app.bot.handlers.debug import dbg_state_cmd
async def spam_import_redirect(update, ctx):
await update.effective_message.reply_text(
"Эту команду нужно выполнить в личке со мной. Откройте чат со мной и пришлите /spam_import."
)
# роутеры
from app.bot.routers.private import register_private_handlers
from app.bot.routers.group import register_group_handlers
from app.bot.routers.channel import register_channel_handlers
def main():
cfg = load_config()
logging.basicConfig(level=getattr(logging, cfg.log_level.upper(), logging.INFO))
# скрыть шум httpx/httpcore
# приглушим шум httpx/httpcore
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpx").propagate = False
logging.getLogger("httpcore").setLevel(logging.WARNING)
@@ -176,68 +35,33 @@ def main():
app = ApplicationBuilder().token(cfg.bot_token).build()
# --- Commands ---
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_cmd))
app.add_handler(CommandHandler("groups", groups_cmd))
app.add_handler(CommandHandler("add_group", add_group_cmd))
app.add_handler(CommandHandler("new", new_cmd))
app.add_handler(CommandHandler("id", chat_id_cmd))
app.add_handler(CommandHandler("mod_status", mod_status_cmd))
app.add_handler(CommandHandler("cancel", cancel_cmd))
app.add_handler(CommandHandler("dbg_state", dbg_state_cmd))
# === РОУТЕРЫ по типам чатов ===
register_private_handlers(app) # ЛС
register_group_handlers(app) # группы/супергруппы
register_channel_handlers(app) # каналы
# --- Callbacks (от узких к широким) ---
# === УЗКИЕ callback-и, общий для редактора ===
from telegram.ext import CallbackQueryHandler
# настройки защитника
app.add_handler(CallbackQueryHandler(security_cb, pattern=r"^pol:"))
app.add_handler(CallbackQueryHandler(dicts_cb, pattern=r"^dict:"))
# привязка канала кнопкой «Привязать этот канал»
app.add_handler(CallbackQueryHandler(bind_chat_cb, pattern=r"^bind:"))
# редактор/мультивыбор (включаем все draft_* и тумблеры)
app.add_handler(CallbackQueryHandler(
on_callback,
pattern=r"^(draft_.*|tgl:|selall:|clear:|sendmulti:)"
))
# (опционально) текстовые команды управления словарями (и в ЛС, и в группе)
from telegram.ext import CommandHandler
app.add_handler(CommandHandler("dicts_link", dicts_link_cmd))
app.add_handler(CommandHandler("dicts_unlink", dicts_unlink_cmd))
app.add_handler(CommandHandler("dicts_link_all", dicts_link_all_cmd))
app.add_handler(CommandHandler("dicts_unlink_all", dicts_unlink_all_cmd))
app.add_handler(CommandHandler("dicts_linked", dicts_linked_cmd))
# --- Private chat helpers ---
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.FORWARDED, add_group_capture))
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & (filters.PHOTO | filters.VIDEO | filters.ANIMATION), on_media))
# --- Join/rights updates ---
app.add_handler(ChatMemberHandler(on_my_chat_member, chat_member_types=ChatMemberHandler.MY_CHAT_MEMBER))
# --- Security / Dict ---
app.add_handler(CommandHandler("security", security_cmd))
# /spam_import — только в ЛС
app.add_handler(CommandHandler("spam_import", spam_import_cmd, filters.ChatType.PRIVATE))
# редирект из групп
app.add_handler(CommandHandler("spam_import", spam_import_redirect, filters.ChatType.GROUPS))
# файл словаря
app.add_handler(MessageHandler(filters.ChatType.PRIVATE & filters.Document.ALL, spam_import_capture))
# --- Private text handlers (ПОРЯДОК КРИТИЧЕН!) ---
# 1) ШЛЮЗ: если ждём текст поста (await_text) — перехватываем и зовём on_text, блокируя цепочку
app.add_handler(MessageHandler(
filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND,
on_text_gate,
block=True
))
# 2) Импорт словаря текстом — НЕ блокирует цепочку
app.add_handler(MessageHandler(
filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND,
spam_import_text_capture,
block=False
))
# 3) Обычный редактор — последним
app.add_handler(MessageHandler(
filters.ChatType.PRIVATE & filters.TEXT & ~filters.COMMAND,
on_text
))
# --- Moderation in groups ---
app.add_handler(MessageHandler(filters.ChatType.GROUPS & ~filters.COMMAND, moderate_message))
# --- Errors ---
# ошибки
app.add_error_handler(on_error)
app.run_polling(allowed_updates=None)

View File

@@ -5,4 +5,4 @@ global:
scrape_configs:
- job_name: "tg_bot"
static_configs:
- targets: ["bot:9100"] # бот слушает метрики на 9100 внутри сети compose
- targets: ["bot:9100"]