Модуль комментирования постов в канале
This commit is contained in:
799
bot/handlers/chl_comment.py
Normal file
799
bot/handlers/chl_comment.py
Normal file
@@ -0,0 +1,799 @@
|
||||
"""
|
||||
Автоматическая отправка комментариев под постами канала (через discussion group)
|
||||
|
||||
+ меню настройки (FSM)
|
||||
+ полная диагностика
|
||||
+ ДИНАМИЧЕСКИЕ КАНАЛЫ ИЗ БД (без .env!)
|
||||
|
||||
ВАЖНО:
|
||||
- Комментарии в Telegram — это reply в привязанной группе обсуждений.
|
||||
- Поэтому ловим auto-forward сообщения в группе: Message.is_automatic_forward == True.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Optional, Tuple, Dict, Any, List
|
||||
|
||||
from aiogram import Router, F, Bot
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.filters import Command
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.utils.markdown import hide_link
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
from configs import settings, COMMANDS
|
||||
from database import get_manager, AutoComment
|
||||
from middleware.loggers import logger
|
||||
from bot.filters.admin import IsAdmin
|
||||
from bot.utils import log_action, tg_emoji
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="channel_comments_router")
|
||||
|
||||
# ======================================================================
|
||||
# FSM STATES
|
||||
# ======================================================================
|
||||
|
||||
class CommentEditStates(StatesGroup):
|
||||
"""Состояния для редактирования комментариев"""
|
||||
selecting_channel = State()
|
||||
waiting_text = State()
|
||||
waiting_button_text = State()
|
||||
waiting_button_url = State()
|
||||
waiting_photo_url = State()
|
||||
waiting_add_channel = State() # ✅ ДОБАВИЛИ
|
||||
|
||||
# ======================================================================
|
||||
# HELPERS
|
||||
# ======================================================================
|
||||
|
||||
def _defaults() -> dict:
|
||||
return {
|
||||
"text": settings.AUTO_COMMENT_TEXT,
|
||||
"button_text": settings.AUTO_COMMENT_BUTTON_TEXT,
|
||||
"button_url": settings.AUTO_COMMENT_BUTTON_URL,
|
||||
"photo_url": settings.AUTO_COMMENT_PHOTO_URL,
|
||||
"is_enabled": False,
|
||||
}
|
||||
|
||||
def _render_menu_text(channel_id: int, config: dict) -> str:
|
||||
status_emoji = "✅ Включено" if config.get("is_enabled") else "❌ Выключено"
|
||||
|
||||
text = config.get("text") or ""
|
||||
photo_url = config.get("photo_url") or ""
|
||||
text_preview = (text[:100] + "...") if len(text) > 100 else text
|
||||
photo_preview = (photo_url[:60] + "...") if len(photo_url) > 60 else photo_url
|
||||
|
||||
return (
|
||||
f"⚙️ <b>НАСТРОЙКА АВТОКОММЕНТАРИЕВ</b>\n\n"
|
||||
f"📢 <b>Канал:</b> <code>{channel_id}</code>\n"
|
||||
f"🔘 <b>Статус:</b> {status_emoji}\n\n"
|
||||
f"📝 <b>Текст:</b>\n{text_preview or '<i>(пусто)</i>'}\n\n"
|
||||
f"🔘 <b>Кнопка:</b> {config.get('button_text') or '<i>(нет)</i>'}\n"
|
||||
f"🔗 <b>URL:</b> <code>{config.get('button_url') or ''}</code>\n\n"
|
||||
f"🖼 <b>Фото:</b>\n<code>{photo_preview}</code>\n\n"
|
||||
f"💡 Выберите действие:"
|
||||
)
|
||||
|
||||
def create_main_menu(channel_id: int) -> InlineKeyboardBuilder:
|
||||
"""Создаёт главное меню управления автокомментариями"""
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text="📝 Текст комментария", callback_data=f"edit:{channel_id}:text")
|
||||
ikb.button(text="🔘 Кнопка", callback_data=f"edit:{channel_id}:button")
|
||||
ikb.button(text="🖼 Фото (скрытое)", callback_data=f"edit:{channel_id}:photo")
|
||||
ikb.button(text="👁 Предпросмотр", callback_data=f"edit:{channel_id}:preview")
|
||||
ikb.button(text="🔄 Переключить", callback_data=f"edit:{channel_id}:toggle")
|
||||
ikb.button(text="🔍 Диагностика", callback_data=f"edit:{channel_id}:diagnostic")
|
||||
ikb.button(text="➕ Добавить канал", callback_data="add_channel") # ✅ ДОБАВИЛИ
|
||||
ikb.button(text="🗑 Удалить", callback_data=f"edit:{channel_id}:delete")
|
||||
ikb.button(text="❌ Закрыть", callback_data="menu:close")
|
||||
ikb.adjust(2, 2, 2, 2, 1)
|
||||
return ikb
|
||||
|
||||
def create_channels_menu(channels: List[int]) -> InlineKeyboardBuilder(): # ✅ List[int]
|
||||
"""Создаёт меню выбора канала"""
|
||||
ikb = InlineKeyboardBuilder()
|
||||
for channel_id in channels:
|
||||
ikb.button(text=f"Канал {channel_id}", callback_data=f"select_channel:{channel_id}")
|
||||
ikb.button(text="➕ Добавить канал", callback_data="add_channel") # ✅ ДОБАВИЛИ
|
||||
ikb.button(text="❌ Закрыть", callback_data="menu:close")
|
||||
ikb.adjust(1)
|
||||
return ikb
|
||||
|
||||
async def get_all_channels() -> List[int]: # ✅ ✅ ✅ ИСПРАВЛЕНО: async!
|
||||
"""Получает ВСЕ каналы из БД"""
|
||||
manager = get_manager()
|
||||
return await manager.get_auto_comment_channels()
|
||||
|
||||
def _build_comment_payload(config: dict) -> Tuple[str, InlineKeyboardBuilder]:
|
||||
photo_url = (config.get("photo_url") or "").strip()
|
||||
text = config.get("text") or ""
|
||||
|
||||
full_text = (hide_link(photo_url) if photo_url else "") + text
|
||||
|
||||
keyboard = InlineKeyboardBuilder()
|
||||
if config.get("button_text") and config.get("button_url"):
|
||||
keyboard.button(text=config["button_text"], url=config["button_url"])
|
||||
return full_text, keyboard
|
||||
|
||||
def _extract_origin_channel_id(message: Message) -> Optional[int]:
|
||||
if not message.is_automatic_forward:
|
||||
return None
|
||||
if message.forward_from_chat and message.forward_from_chat.type == "channel":
|
||||
return message.forward_from_chat.id
|
||||
return None
|
||||
|
||||
# Дедуп: чтобы не комментировать каждый элемент альбома (media_group_id)
|
||||
_MEDIA_GROUP_SEEN: Dict[tuple[int, str], float] = {}
|
||||
_MEDIA_GROUP_TTL_SEC = 45.0
|
||||
|
||||
def _media_group_should_skip(message: Message) -> bool:
|
||||
if not message.media_group_id:
|
||||
return False
|
||||
|
||||
now = time.time()
|
||||
key = (message.chat.id, str(message.media_group_id))
|
||||
last = _MEDIA_GROUP_SEEN.get(key)
|
||||
|
||||
if len(_MEDIA_GROUP_SEEN) > 500:
|
||||
cutoff = now - _MEDIA_GROUP_TTL_SEC
|
||||
for k, t in list(_MEDIA_GROUP_SEEN.items()):
|
||||
if t < cutoff:
|
||||
_MEDIA_GROUP_SEEN.pop(k, None)
|
||||
|
||||
if last and (now - last) < _MEDIA_GROUP_TTL_SEC:
|
||||
return True
|
||||
|
||||
_MEDIA_GROUP_SEEN[key] = now
|
||||
return False
|
||||
|
||||
async def get_channel_config(channel_id: int) -> dict:
|
||||
"""
|
||||
Получает настройки автокомментариев для канала из БД.
|
||||
Ничего "не затирает": если поля отсутствуют — подставляет дефолты.
|
||||
"""
|
||||
manager = get_manager()
|
||||
config = await manager.get_auto_comment_settings(channel_id) or {}
|
||||
|
||||
merged = _defaults()
|
||||
merged.update({k: v for k, v in config.items() if v is not None})
|
||||
|
||||
if "is_enabled" not in config:
|
||||
merged["is_enabled"] = False
|
||||
|
||||
return merged
|
||||
|
||||
async def _persist_settings_preserve_enabled(
|
||||
channel_id: int,
|
||||
patch: dict,
|
||||
updated_by: int,
|
||||
) -> bool:
|
||||
"""
|
||||
Надёжное сохранение настроек:
|
||||
- всегда делает "первичное сохранение" через save_auto_comment_settings (чтобы запись точно появилась)
|
||||
- сохраняет старый is_enabled (если было выключено — выключаем обратно после сохранения)
|
||||
"""
|
||||
manager = get_manager()
|
||||
|
||||
raw = await manager.get_auto_comment_settings(channel_id) or {}
|
||||
was_enabled = bool(raw.get("is_enabled", False))
|
||||
|
||||
merged = _defaults()
|
||||
merged.update({k: v for k, v in raw.items() if v is not None})
|
||||
merged.update({k: v for k, v in patch.items() if v is not None})
|
||||
|
||||
# save_auto_comment_settings у тебя уже используется при включении (значит умеет создавать запись)
|
||||
success = await manager.save_auto_comment_settings(
|
||||
channel_id=channel_id,
|
||||
text=merged.get("text") or "",
|
||||
button_text=merged.get("button_text") or "",
|
||||
button_url=merged.get("button_url") or "",
|
||||
photo_url=merged.get("photo_url") or "",
|
||||
updated_by=updated_by,
|
||||
)
|
||||
if not success:
|
||||
return False
|
||||
|
||||
# Если было выключено — сохраняем выключенным (на случай если save_* включает фичу)
|
||||
if not was_enabled:
|
||||
try:
|
||||
await manager.repo.toggle_auto_comment(
|
||||
channel_id=channel_id,
|
||||
is_enabled=False,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"toggle_auto_comment failed (preserve disabled): {e}", log_type="CHANNEL")
|
||||
|
||||
return True
|
||||
|
||||
# ======================================================================
|
||||
# CORE: AUTO COMMENTS (discussion group) ✅ ФИКС #3
|
||||
# ======================================================================
|
||||
|
||||
@router.message(F.is_automatic_forward)
|
||||
async def auto_comment_from_discussion_forward(message: Message) -> None:
|
||||
if _media_group_should_skip(message):
|
||||
logger.debug(
|
||||
f"⏭ Skip media_group duplicate: chat={message.chat.id} media_group_id={message.media_group_id}",
|
||||
log_type="CHANNEL"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"📥 Discussion forward received: chat={message.chat.id}, msg_id={message.message_id}, "
|
||||
f"is_auto={message.is_automatic_forward}, forward_from_chat={getattr(message.forward_from_chat, 'id', None)}",
|
||||
log_type="CHANNEL"
|
||||
)
|
||||
|
||||
channel_id = _extract_origin_channel_id(message)
|
||||
if not channel_id:
|
||||
logger.warning(
|
||||
f"❌ Cannot extract origin channel id for msg={message.message_id} in chat={message.chat.id}",
|
||||
log_type="CHANNEL"
|
||||
)
|
||||
return
|
||||
|
||||
channels = await get_all_channels() # ✅ ✅ ✅ ФИКС #3: await!
|
||||
if not channels:
|
||||
return
|
||||
if channel_id not in channels:
|
||||
return
|
||||
|
||||
is_test = False
|
||||
txt = message.text or message.caption or ""
|
||||
if "/test_comment" in txt:
|
||||
is_test = True
|
||||
|
||||
try:
|
||||
config = await get_channel_config(channel_id)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Config load failed for channel={channel_id}: {e}", log_type="CHANNEL")
|
||||
return
|
||||
|
||||
if not config.get("is_enabled") and not is_test:
|
||||
logger.debug(f"⏭ Auto-comments disabled for channel={channel_id}", log_type="CHANNEL")
|
||||
return
|
||||
|
||||
try:
|
||||
full_text, keyboard = _build_comment_payload(config)
|
||||
|
||||
sent = await message.reply(
|
||||
text=full_text,
|
||||
reply_markup=keyboard.as_markup(),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
logger.success(
|
||||
f"✅ Comment sent (discussion reply)\n"
|
||||
f" ├─ Origin channel: {channel_id}\n"
|
||||
f" ├─ Discussion chat: {message.chat.id}\n"
|
||||
f" ├─ Forward msg id: {message.message_id}\n"
|
||||
f" ├─ Comment msg id: {sent.message_id}\n"
|
||||
f" └─ Test mode: {is_test}",
|
||||
log_type="CHANNEL"
|
||||
)
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
logger.error(
|
||||
f"❌ TelegramBadRequest while sending comment: {e}\n"
|
||||
f" channel={channel_id} discussion_chat={message.chat.id} msg={message.message_id}",
|
||||
log_type="CHANNEL"
|
||||
)
|
||||
except TelegramForbiddenError as e:
|
||||
logger.error(
|
||||
f"❌ TelegramForbiddenError while sending comment: {e}\n"
|
||||
f" Bot likely has no rights to write in discussion group.\n"
|
||||
f" channel={channel_id} discussion_chat={message.chat.id}",
|
||||
log_type="CHANNEL"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Unexpected error while sending comment: {e}", log_type="CHANNEL")
|
||||
|
||||
# ======================================================================
|
||||
# ✅ НОВЫЕ ХЕНДЛЕРЫ ДЛЯ ДОБАВЛЕНИЯ КАНАЛА
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data == "add_channel", IsAdmin())
|
||||
async def add_channel_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
await state.update_data(action="add_channel")
|
||||
await state.set_state(CommentEditStates.waiting_add_channel)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text=(
|
||||
"➕ <b>ДОБАВИТЬ КАНАЛ</b>\n\n"
|
||||
"Отправьте ID канала (число с минусом):\n"
|
||||
"<code>Пример: -1003876862007</code>\n\n"
|
||||
"💡 @userinfobot для получения ID\n\n"
|
||||
"Для отмены: /cancel"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@router.message(CommentEditStates.waiting_add_channel, IsAdmin())
|
||||
async def process_add_channel(message: Message, state: FSMContext) -> None:
|
||||
if message.text == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer("❌ Отменено")
|
||||
return
|
||||
|
||||
try:
|
||||
channel_id = int(message.text.strip())
|
||||
if not str(channel_id).startswith('-'):
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный ID. Пример: <code>-1003876862007</code>", parse_mode="HTML")
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
success = await manager.add_auto_comment_channel(channel_id, message.from_user.id)
|
||||
|
||||
await state.clear()
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ <b>Канал добавлен!</b>\n<code>{channel_id}</code>\n/redactcomment", parse_mode="HTML")
|
||||
else:
|
||||
await message.answer(f"❌ Канал <code>{channel_id}</code> уже существует!", parse_mode="HTML")
|
||||
|
||||
# ======================================================================
|
||||
# DIAGNOSTICS ✅ ФИКС #2
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data.regexp(r"edit:(-?\d+):diagnostic"))
|
||||
async def diagnostic_channel_callback(callback: CallbackQuery) -> None:
|
||||
channel_id = int(callback.data.split(":")[1])
|
||||
bot: Bot = callback.bot
|
||||
|
||||
await callback.answer("🔍 Запуск диагностики...", show_alert=False)
|
||||
|
||||
diagnostic_text = "🔍 <b>ДИАГНОСТИКА АВТОКОММЕНТАРИЕВ</b>\n\n"
|
||||
|
||||
channels = await get_all_channels() # ✅ ✅ ✅ ФИКС #2: await!
|
||||
diagnostic_text += "1️⃣ <b>Настройки:</b>\n"
|
||||
diagnostic_text += f" ├─ Каналы из БД: <code>{channels}</code>\n"
|
||||
diagnostic_text += f" └─ Канал в списке: {'✅' if channel_id in channels else '❌'}\n\n"
|
||||
|
||||
diagnostic_text += "2️⃣ <b>База данных:</b>\n"
|
||||
try:
|
||||
config = await get_channel_config(channel_id)
|
||||
diagnostic_text += " ├─ Настройки читаются: ✅\n"
|
||||
diagnostic_text += f" ├─ Статус: {'✅ Включено' if config.get('is_enabled') else '❌ Выключено'}\n"
|
||||
diagnostic_text += f" ├─ Текст: {len(config.get('text') or '')} символов\n"
|
||||
diagnostic_text += f" ├─ Кнопка: {('✅' if (config.get('button_text') and config.get('button_url')) else '❌')}\n"
|
||||
diagnostic_text += f" └─ Фото URL: {('✅' if bool(config.get('photo_url')) else '❌')}\n\n"
|
||||
except Exception as e:
|
||||
diagnostic_text += f" └─ ❌ Ошибка: {e}\n\n"
|
||||
config = None
|
||||
|
||||
diagnostic_text += "3️⃣ <b>Бот в канале:</b>\n"
|
||||
try:
|
||||
member = await bot.get_chat_member(channel_id, bot.id)
|
||||
diagnostic_text += f" ├─ Статус: <code>{member.status}</code>\n"
|
||||
if member.status == "administrator":
|
||||
diagnostic_text += " ├─ Админ: ✅\n"
|
||||
if hasattr(member, "can_post_messages"):
|
||||
diagnostic_text += f" └─ can_post_messages: {'✅' if member.can_post_messages else '❌'}\n"
|
||||
else:
|
||||
diagnostic_text += " └─ can_post_messages: (нет поля у этого типа)\n"
|
||||
elif member.status == "creator":
|
||||
diagnostic_text += " └─ Создатель: ✅\n"
|
||||
else:
|
||||
diagnostic_text += " └─ НЕ админ: ❌\n"
|
||||
diagnostic_text += "\n"
|
||||
except TelegramForbiddenError:
|
||||
diagnostic_text += " └─ ❌ Бот не в канале / нет доступа\n\n"
|
||||
except Exception as e:
|
||||
diagnostic_text += f" └─ ❌ Ошибка: {e}\n\n"
|
||||
|
||||
diagnostic_text += "4️⃣ <b>Привязанная группа обсуждений:</b>\n"
|
||||
linked_chat_id = None
|
||||
try:
|
||||
chat = await bot.get_chat(channel_id)
|
||||
linked_chat_id = getattr(chat, "linked_chat_id", None)
|
||||
if linked_chat_id:
|
||||
diagnostic_text += " ├─ linked_chat_id: ✅\n"
|
||||
diagnostic_text += f" └─ ID: <code>{linked_chat_id}</code>\n\n"
|
||||
else:
|
||||
diagnostic_text += " └─ ❌ Не подключена (linked_chat_id отсутствует)\n\n"
|
||||
except Exception as e:
|
||||
diagnostic_text += f" └─ ❌ Ошибка: {e}\n\n"
|
||||
|
||||
diagnostic_text += "5️⃣ <b>Бот в группе обсуждений:</b>\n"
|
||||
if not linked_chat_id:
|
||||
diagnostic_text += " └─ ⏭ Пропущено (группа не найдена)\n\n"
|
||||
else:
|
||||
try:
|
||||
gmember = await bot.get_chat_member(linked_chat_id, bot.id)
|
||||
diagnostic_text += f" ├─ Статус: <code>{gmember.status}</code>\n"
|
||||
if gmember.status in ("administrator", "creator", "member"):
|
||||
diagnostic_text += " ├─ Присутствует: ✅\n"
|
||||
else:
|
||||
diagnostic_text += " ├─ Присутствует: ❌\n"
|
||||
|
||||
if hasattr(gmember, "can_send_messages"):
|
||||
diagnostic_text += f" └─ can_send_messages: {'✅' if gmember.can_send_messages else '❌'}\n\n"
|
||||
else:
|
||||
diagnostic_text += " └─ can_send_messages: (нет поля у этого типа)\n\n"
|
||||
except TelegramForbiddenError:
|
||||
diagnostic_text += " └─ ❌ Бот не в группе / нет доступа\n\n"
|
||||
except Exception as e:
|
||||
diagnostic_text += f" └─ ❌ Ошибка: {e}\n\n"
|
||||
|
||||
diagnostic_text += "💡 <b>Что должно быть для работы:</b>\n"
|
||||
if channel_id not in channels:
|
||||
diagnostic_text += " • Добавьте канал ➕\n"
|
||||
diagnostic_text += (
|
||||
" • Включите автокомментарии (🔄 Переключить)\n"
|
||||
" • Подключите discussion group к каналу\n"
|
||||
" • Дайте боту право писать в группе обсуждений\n"
|
||||
" • Для теста: отправьте пост в канал или пост с /test_comment\n"
|
||||
)
|
||||
|
||||
if callback.message:
|
||||
await callback.message.answer(text=diagnostic_text, parse_mode="HTML")
|
||||
|
||||
# ======================================================================
|
||||
# ADMIN UI: COMMAND + MENUS ✅ ФИКС #1
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data.casefold() == "redactcomment", IsAdmin())
|
||||
@router.message(Command(*COMMANDS["redactcomment"], prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="START_COMMAND", log_args=True)
|
||||
async def redact_comment_cmd(message: Message, state: FSMContext) -> None:
|
||||
channels = await get_all_channels() # ✅ ✅ ✅ ФИКС #1: await!
|
||||
|
||||
await state.clear()
|
||||
|
||||
if not channels:
|
||||
await message.answer(
|
||||
"📢 <b>УПРАВЛЕНИЕ АВТОКОММЕНТАРИЯМИ</b>\n\n"
|
||||
"🚫 <b>Каналы не настроены</b>\n\n"
|
||||
"👆 <b>➕ Добавить канал</b>",
|
||||
reply_markup=create_channels_menu([]).as_markup(), # ✅ Пустое + кнопка
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
if len(channels) == 1:
|
||||
await show_channel_menu(message, channels[0])
|
||||
else:
|
||||
await message.answer(
|
||||
"📢 <b>Выберите канал:</b>",
|
||||
reply_markup=create_channels_menu(channels).as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
async def show_channel_menu(message: Message, channel_id: int) -> None:
|
||||
config = await get_channel_config(channel_id)
|
||||
output = _render_menu_text(channel_id, config)
|
||||
|
||||
await message.answer(
|
||||
text=output,
|
||||
reply_markup=create_main_menu(channel_id).as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("select_channel:"))
|
||||
async def select_channel_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
channel_id = int(callback.data.split(":")[1])
|
||||
await state.clear()
|
||||
|
||||
config = await get_channel_config(channel_id)
|
||||
output = _render_menu_text(channel_id, config)
|
||||
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text=output,
|
||||
reply_markup=create_main_menu(channel_id).as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
# ======================================================================
|
||||
# EDIT TEXT
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data.regexp(r"edit:(-?\d+):text"), IsAdmin())
|
||||
async def edit_text_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
channel_id = int(callback.data.split(":")[1])
|
||||
|
||||
await state.update_data(channel_id=channel_id)
|
||||
await state.set_state(CommentEditStates.waiting_text)
|
||||
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text=(
|
||||
"📝 <b>РЕДАКТИРОВАНИЕ ТЕКСТА</b>\n\n"
|
||||
"Отправьте новый текст комментария.\n\n"
|
||||
"💡 <b>Поддерживается HTML</b>\n\n"
|
||||
"Для отмены: /cancel"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@router.message(CommentEditStates.waiting_text, IsAdmin())
|
||||
async def process_text_input(message: Message, state: FSMContext) -> None:
|
||||
if (message.text or "").strip() == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer("❌ Отменено")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
channel_id = data.get("channel_id")
|
||||
if not channel_id:
|
||||
await state.clear()
|
||||
await message.answer("❌ Не выбран канал. Откройте меню заново: /redactcomment")
|
||||
return
|
||||
|
||||
new_text = message.text or ""
|
||||
|
||||
try:
|
||||
success = await _persist_settings_preserve_enabled(
|
||||
channel_id=int(channel_id),
|
||||
patch={"text": new_text},
|
||||
updated_by=message.from_user.id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"update text failed: {e}", log_type="CHANNEL")
|
||||
success = False
|
||||
|
||||
await state.clear()
|
||||
|
||||
if not success:
|
||||
await message.answer(
|
||||
"❌ <b>Ошибка сохранения</b>\n\nПопробуйте ещё раз через /redactcomment",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
await message.answer("✅ <b>Текст обновлён!</b>", parse_mode="HTML")
|
||||
await show_channel_menu(message, int(channel_id))
|
||||
|
||||
# ======================================================================
|
||||
# EDIT BUTTON
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data.regexp(r"edit:(-?\d+):button"), IsAdmin())
|
||||
async def edit_button_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
channel_id = int(callback.data.split(":")[1])
|
||||
|
||||
await state.update_data(channel_id=channel_id)
|
||||
await state.set_state(CommentEditStates.waiting_button_text)
|
||||
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text=(
|
||||
"🔘 <b>РЕДАКТИРОВАНИЕ КНОПКИ</b>\n\n"
|
||||
"<b>Шаг 1 из 2:</b> Отправьте текст кнопки\n\n"
|
||||
"Для отмены: /cancel"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@router.message(CommentEditStates.waiting_button_text, IsAdmin())
|
||||
async def process_button_text(message: Message, state: FSMContext) -> None:
|
||||
if (message.text or "").strip() == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer("❌ Отменено")
|
||||
return
|
||||
|
||||
await state.update_data(button_text=message.text or "")
|
||||
await state.set_state(CommentEditStates.waiting_button_url)
|
||||
|
||||
await message.answer(
|
||||
text=(
|
||||
f"✅ Текст кнопки: <b>{(message.text or '').strip()}</b>\n\n"
|
||||
f"<b>Шаг 2 из 2:</b> Отправьте URL кнопки\n\n"
|
||||
f"Для отмены: /cancel"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
@router.message(CommentEditStates.waiting_button_url, IsAdmin())
|
||||
async def process_button_url(message: Message, state: FSMContext) -> None:
|
||||
if (message.text or "").strip() == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer("❌ Отменено")
|
||||
return
|
||||
|
||||
url = (message.text or "").strip()
|
||||
if not url.startswith(("http://", "https://")):
|
||||
await message.answer(
|
||||
"❌ <b>Неверный формат URL</b>\n\nURL должен начинаться с http:// или https://",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
channel_id = data.get("channel_id")
|
||||
button_text = (data.get("button_text") or "").strip()
|
||||
|
||||
if not channel_id:
|
||||
await state.clear()
|
||||
await message.answer("❌ Не выбран канал. Откройте меню заново: /redactcomment")
|
||||
return
|
||||
|
||||
try:
|
||||
success = await _persist_settings_preserve_enabled(
|
||||
channel_id=int(channel_id),
|
||||
patch={"button_text": button_text, "button_url": url},
|
||||
updated_by=message.from_user.id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"update button failed: {e}", log_type="CHANNEL")
|
||||
success = False
|
||||
|
||||
await state.clear()
|
||||
|
||||
if not success:
|
||||
await message.answer("❌ <b>Ошибка сохранения</b>\n\nПопробуйте ещё раз через /redactcomment", parse_mode="HTML")
|
||||
return
|
||||
|
||||
await message.answer("✅ <b>Кнопка обновлена!</b>", parse_mode="HTML")
|
||||
await show_channel_menu(message, int(channel_id))
|
||||
|
||||
# ======================================================================
|
||||
# EDIT PHOTO URL
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data.regexp(r"edit:(-?\d+):photo"), IsAdmin())
|
||||
async def edit_photo_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
channel_id = int(callback.data.split(":")[1])
|
||||
|
||||
await state.update_data(channel_id=channel_id)
|
||||
await state.set_state(CommentEditStates.waiting_photo_url)
|
||||
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text=(
|
||||
"🖼 <b>РЕДАКТИРОВАНИЕ ФОТО</b>\n\n"
|
||||
"Отправьте прямую ссылку на изображение (http/https).\n\n"
|
||||
"Для отмены: /cancel"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@router.message(CommentEditStates.waiting_photo_url, IsAdmin())
|
||||
async def process_photo_url(message: Message, state: FSMContext) -> None:
|
||||
if (message.text or "").strip() == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer("❌ Отменено")
|
||||
return
|
||||
|
||||
url = (message.text or "").strip()
|
||||
if not url.startswith(("http://", "https://")):
|
||||
await message.answer(
|
||||
"❌ <b>Неверный формат URL</b>\n\nURL должен начинаться с http:// или https://",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
channel_id = data.get("channel_id")
|
||||
if not channel_id:
|
||||
await state.clear()
|
||||
await message.answer("❌ Не выбран канал. Откройте меню заново: /redactcomment")
|
||||
return
|
||||
|
||||
try:
|
||||
success = await _persist_settings_preserve_enabled(
|
||||
channel_id=int(channel_id),
|
||||
patch={"photo_url": url},
|
||||
updated_by=message.from_user.id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"update photo failed: {e}", log_type="CHANNEL")
|
||||
success = False
|
||||
|
||||
await state.clear()
|
||||
|
||||
if not success:
|
||||
await message.answer("❌ <b>Ошибка сохранения</b>\n\nПопробуйте ещё раз через /redactcomment", parse_mode="HTML")
|
||||
return
|
||||
|
||||
await message.answer(hide_link(url) + "✅ <b>Фото обновлено!</b>", parse_mode="HTML")
|
||||
await show_channel_menu(message, int(channel_id))
|
||||
|
||||
# ======================================================================
|
||||
# PREVIEW
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data.regexp(r"edit:(-?\d+):preview"), IsAdmin())
|
||||
async def preview_comment_callback(callback: CallbackQuery) -> None:
|
||||
channel_id = int(callback.data.split(":")[1])
|
||||
config = await get_channel_config(channel_id)
|
||||
|
||||
full_text, keyboard = _build_comment_payload(config)
|
||||
|
||||
if callback.message:
|
||||
await callback.message.answer(
|
||||
text=f"👁 <b>ПРЕВЬЮ КОММЕНТАРИЯ</b>\n\n{full_text}",
|
||||
reply_markup=keyboard.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer("✅ Превью отправлено")
|
||||
|
||||
# ======================================================================
|
||||
# TOGGLE
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data.regexp(r"edit:(-?\d+):toggle"), IsAdmin())
|
||||
async def toggle_comment_callback(callback: CallbackQuery) -> None:
|
||||
channel_id = int(callback.data.split(":")[1])
|
||||
|
||||
config = await get_channel_config(channel_id)
|
||||
current_status = bool(config.get("is_enabled"))
|
||||
new_status = not current_status
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
if new_status:
|
||||
success = await manager.save_auto_comment_settings(
|
||||
channel_id=channel_id,
|
||||
text=config.get("text") or "",
|
||||
button_text=config.get("button_text") or "",
|
||||
button_url=config.get("button_url") or "",
|
||||
photo_url=config.get("photo_url") or "",
|
||||
updated_by=callback.from_user.id
|
||||
)
|
||||
else:
|
||||
success = await manager.repo.toggle_auto_comment(
|
||||
channel_id=channel_id,
|
||||
is_enabled=False,
|
||||
updated_by=callback.from_user.id
|
||||
)
|
||||
|
||||
if not success:
|
||||
await callback.answer("❌ Ошибка переключения", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.answer(f"Автокомментарии {'✅ включены' if new_status else '❌ выключены'}", show_alert=True)
|
||||
|
||||
config = await get_channel_config(channel_id)
|
||||
output = _render_menu_text(channel_id, config)
|
||||
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text=output,
|
||||
reply_markup=create_main_menu(channel_id).as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# ======================================================================
|
||||
# DELETE SETTINGS
|
||||
# ======================================================================
|
||||
|
||||
@router.callback_query(F.data.regexp(r"edit:(-?\d+):delete"), IsAdmin())
|
||||
async def delete_comment_callback(callback: CallbackQuery) -> None:
|
||||
channel_id = int(callback.data.split(":")[1])
|
||||
|
||||
manager = get_manager()
|
||||
success = await manager.repo.delete_auto_comment(channel_id)
|
||||
|
||||
if not success:
|
||||
await callback.answer("❌ Ошибка удаления", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.answer("🗑 Настройки удалены", show_alert=True)
|
||||
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text=(
|
||||
"🗑 <b>НАСТРОЙКИ УДАЛЕНЫ</b>\n\n"
|
||||
f"Автокомментарии для канала <code>{channel_id}</code> удалены.\n\n"
|
||||
"Будут использоваться настройки по умолчанию из .env\n\n"
|
||||
"Для настройки: /redactcomment"
|
||||
),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user