Астат ты не вознесешься

This commit is contained in:
2026-02-18 01:43:22 +07:00
parent 59a3a7b46a
commit 5d350d0885
15 changed files with 1489 additions and 183 deletions

770
bot/handlers/chl_comment.py Normal file
View File

@@ -0,0 +1,770 @@
"""
Автоматическая отправка комментариев под постами канала (через discussion group)
+ меню настройки (FSM)
+ полная диагностика
ВАЖНО:
- Комментарии в Telegram — это reply в привязанной группе обсуждений.
- Поэтому ловим auto-forward сообщения в группе: Message.is_automatic_forward == True.
"""
from __future__ import annotations
import time
from typing import Optional, Tuple, Dict
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
from database import get_manager
from middleware.loggers import logger
from bot.filters.admin import IsAdmin
__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()
# ======================================================================
# 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,
}
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})
# Если в БД is_enabled=False, пользовательские поля (текст/кнопка/фото) сохраняем
# и просто считаем фичу выключенной.
if "is_enabled" not in config:
merged["is_enabled"] = False
return merged
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=f"edit:{channel_id}:delete")
ikb.button(text="❌ Закрыть", callback_data="menu:close")
ikb.adjust(2, 2, 2, 1, 1)
return ikb
def create_channels_menu(channels: list[int]) -> InlineKeyboardBuilder:
"""Создаёт меню выбора канала"""
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="menu:close")
ikb.adjust(1)
return ikb
def _build_comment_payload(config: dict) -> Tuple[str, InlineKeyboardBuilder]:
full_text = hide_link(config["photo_url"]) + (config["text"] or "")
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]:
"""
Для auto-forward из привязанного канала Telegram обычно проставляет:
- message.is_automatic_forward = True
- message.forward_from_chat = канал
Если forward_from_chat вдруг отсутствует — возвращаем None.
"""
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:
"""
Возвращает True если это повторная часть альбома и мы уже комментировали.
Ключ: (chat_id, media_group_id).
"""
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
# ======================================================================
# CORE: AUTO COMMENTS (discussion group)
# ======================================================================
@router.message(F.is_automatic_forward)
async def auto_comment_from_discussion_forward(message: Message) -> None:
"""
Ловим пост канала, автоматически пересланный в привязанную группу обсуждений.
Комментарий отправляем reply на это сообщение => появляется "под постом".
"""
# 0) Дедуп альбомов
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"
)
# 1) Канал-источник
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
# 2) Проверка списка каналов
channels = settings.AUTO_COMMENT_CHANNELS_LIST
if not channels:
logger.warning("❌ AUTO_COMMENT_CHANNELS_LIST пуст — нечего обрабатывать", log_type="CHANNEL")
return
if channel_id not in channels:
logger.debug(f"⏭ Channel {channel_id} not in configured list", log_type="CHANNEL")
return
# 3) /test_comment (если админ запостил команду в канале — она тоже прилетит сюда автофорвардом)
is_test = False
txt = message.text or message.caption or ""
if "/test_comment" in txt:
is_test = True
# 4) Настройки и статус
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
# 5) Формируем и отправляем комментарий (reply в группе)
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(
"✅ 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",
)
# ======================================================================
# DIAGNOSTICS
# ======================================================================
@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"
# 1) ENV settings
channels = settings.AUTO_COMMENT_CHANNELS_LIST
diagnostic_text += "1⃣ <b>Настройки:</b>\n"
diagnostic_text += f" ├─ AUTO_COMMENT_CHANNELS_LIST: <code>{channels}</code>\n"
diagnostic_text += f" └─ Канал в списке: {'' if channel_id in channels else ''}\n\n"
# 2) DB config
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
# 3) Bot status in channel
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"
# 4) Linked discussion group
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"
# 5) Bot status in discussion group
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"
# can_send_messages бывает не у всех типов, поэтому hasattr
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"
# Recommendations
diagnostic_text += "💡 <b>Что должно быть для работы:</b>\n"
if channel_id not in channels:
diagnostic_text += " • Добавьте канал в AUTO_COMMENT_CHANNELS\n"
diagnostic_text += " • Включите автокомментарии (🔄 Переключить)\n"
diagnostic_text += " • Подключите discussion group к каналу\n"
diagnostic_text += " • Дайте боту право писать в группе обсуждений\n"
diagnostic_text += " • Для теста: отправьте пост в канал или пост с /test_comment\n"
await callback.message.answer(text=diagnostic_text, parse_mode="HTML")
# ======================================================================
# ADMIN UI: COMMAND + MENUS
# ======================================================================
@router.message(Command("redactcomment"), IsAdmin())
async def redact_comment_cmd(message: Message, state: FSMContext) -> None:
"""Открывает меню управления автокомментариями"""
channels = settings.AUTO_COMMENT_CHANNELS_LIST
if not channels:
await message.answer(
"❌ <b>Каналы не настроены</b>\n\n"
"Добавьте ID каналов в .env файл:\n"
"<code>AUTO_COMMENT_CHANNELS=-1003876862007</code>\n\n"
"💡 Узнать ID канала: перешлите пост из канала боту @userinfobot",
parse_mode="HTML"
)
return
if len(channels) == 1:
await show_channel_menu(message, channels[0])
else:
await message.answer(
"📢 <b>УПРАВЛЕНИЕ АВТОКОММЕНТАРИЯМИ</b>\n\n"
"Выберите канал для настройки:",
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)
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
output = (
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"💡 Выберите действие:"
)
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) -> None:
"""Обработка выбора канала из списка"""
channel_id = int(callback.data.split(":")[1])
config = await get_channel_config(channel_id)
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
output = (
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"💡 Выберите действие:"
)
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"))
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)
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)
async def process_text_input(message: Message, state: FSMContext) -> None:
if message.text == "/cancel":
await state.clear()
await message.answer("❌ Отменено")
return
data = await state.get_data()
channel_id = data.get("channel_id")
manager = get_manager()
success = await manager.update_auto_comment_text(
channel_id=channel_id,
text=message.text or "",
updated_by=message.from_user.id
)
await state.clear()
if not success:
await message.answer(
"❌ <b>Ошибка сохранения</b>\n\nПопробуйте ещё раз через /redactcomment",
parse_mode="HTML"
)
return
await message.answer(f"✅ <b>Текст обновлён!</b>", parse_mode="HTML")
await show_channel_menu(message, channel_id)
# ======================================================================
# EDIT BUTTON
# ======================================================================
@router.callback_query(F.data.regexp(r"edit:(-?\d+):button"))
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)
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)
async def process_button_text(message: Message, state: FSMContext) -> None:
if message.text == "/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}</b>\n\n"
f"<b>Шаг 2 из 2:</b> Отправьте URL кнопки\n\n"
f"Для отмены: /cancel"
),
parse_mode="HTML"
)
@router.message(CommentEditStates.waiting_button_url)
async def process_button_url(message: Message, state: FSMContext) -> None:
if message.text == "/cancel":
await state.clear()
await message.answer("❌ Отменено")
return
url = message.text or ""
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 ""
manager = get_manager()
success = await manager.update_auto_comment_button(
channel_id=channel_id,
button_text=button_text,
button_url=url,
updated_by=message.from_user.id
)
await state.clear()
if not success:
await message.answer("❌ Ошибка сохранения", parse_mode="HTML")
return
await message.answer("✅ <b>Кнопка обновлена!</b>", parse_mode="HTML")
await show_channel_menu(message, channel_id)
# ======================================================================
# EDIT PHOTO URL
# ======================================================================
@router.callback_query(F.data.regexp(r"edit:(-?\d+):photo"))
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)
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)
async def process_photo_url(message: Message, state: FSMContext) -> None:
if message.text == "/cancel":
await state.clear()
await message.answer("❌ Отменено")
return
url = message.text or ""
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")
manager = get_manager()
success = await manager.update_auto_comment_photo(
channel_id=channel_id,
photo_url=url,
updated_by=message.from_user.id
)
await state.clear()
if not success:
await message.answer("❌ Ошибка сохранения", parse_mode="HTML")
return
await message.answer(hide_link(url) + "✅ <b>Фото обновлено!</b>", parse_mode="HTML")
await show_channel_menu(message, channel_id)
# ======================================================================
# PREVIEW
# ======================================================================
@router.callback_query(F.data.regexp(r"edit:(-?\d+):preview"))
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)
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"))
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)
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
output = (
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"💡 Выберите действие:"
)
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"))
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)
await callback.message.edit_text(
text=(
"🗑 <b>НАСТРОЙКИ УДАЛЕНЫ</b>\n\n"
f"Автокомментарии для канала <code>{channel_id}</code> удалены.\n\n"
"Будут использоваться настройки по умолчанию из .env\n\n"
"Для настройки: /redactcomment"
),
parse_mode="HTML"
)
# ======================================================================
# CLOSE / CANCEL
# ======================================================================
@router.callback_query(F.data == "menu:close")
async def close_menu_callback(callback: CallbackQuery, state: FSMContext) -> None:
await state.clear()
await callback.message.delete()
await callback.answer("❌ Меню закрыто")
@router.message(Command("cancel"))
async def cancel_handler(message: Message, state: FSMContext) -> None:
current_state = await state.get_state()
if current_state is None:
await message.answer("❌ Нечего отменять")
return
await state.clear()
await message.answer("✅ Действие отменено")