Compare commits
1 Commits
430101eb25
...
refactor
| Author | SHA1 | Date | |
|---|---|---|---|
| 00b00f5bf3 |
249
app/bot/handlers/dict_commands.py
Normal file
249
app/bot/handlers/dict_commands.py
Normal 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))
|
||||
@@ -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()
|
||||
|
||||
@@ -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}"),
|
||||
|
||||
0
app/bot/routers/__init__.py
Normal file
0
app/bot/routers/__init__.py
Normal file
18
app/bot/routers/channel.py
Normal file
18
app/bot/routers/channel.py
Normal 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
28
app/bot/routers/group.py
Normal 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))
|
||||
40
app/bot/routers/private.py
Normal file
40
app/bot/routers/private.py
Normal 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))
|
||||
238
app/main.py
238
app/main.py
@@ -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)
|
||||
|
||||
@@ -5,4 +5,4 @@ global:
|
||||
scrape_configs:
|
||||
- job_name: "tg_bot"
|
||||
static_configs:
|
||||
- targets: ["bot:9100"] # бот слушает метрики на 9100 внутри сети compose
|
||||
- targets: ["bot:9100"]
|
||||
|
||||
Reference in New Issue
Block a user