Files
batch-bot/bot/controller.py
bilal bb27161524 Final release: Multi-session comment bot with filtering
Features:
- Multi-account support (session files)
- AI comments via Ollama
- Telegram bot moderation
- Filter by sessions and groups
- Docker support
- Auto-join groups
- Log notifications
- DB migration script

Bug fixes:
- Fixed comment_to for proper post targeting
- Fixed entity lookup with multiple ID formats
- Fixed callback handlers for filtering
- Added auto-join before entity lookup
2026-02-28 01:44:40 +03:00

1046 lines
44 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import logging
import sqlite3
from datetime import datetime
from aiogram import Bot, Dispatcher, F
from aiogram.filters import Command, CommandStart
from aiogram.types import CallbackQuery, Message
from bot.config import BOT_TOKEN, ADMIN_IDS, LOG_GROUP_ID, INITIAL_SCAN_LIMIT, DB_PATH
from bot.db import (
init_db, get_pending_comments, update_comment_status,
save_comment, get_comment, get_comments_for_post,
increment_regeneration, get_all_sessions, get_active_sessions,
toggle_session, delete_session, get_summary_stats, get_stats,
update_stats, add_target_group, get_target_groups, get_all_target_groups,
remove_target_group, toggle_target_group, is_target_group,
get_pending_comments_by_session, get_pending_comments_by_group
)
from bot.ollama import generate_comment
from bot.keyboard import (
create_moderation_keyboard, create_main_menu, create_main_keyboard,
create_session_keyboard, create_edit_cancel_keyboard,
create_groups_list_keyboard, create_group_action_keyboard
)
from bot.session_manager import session_manager
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('controller')
class CommentBot:
"""Контроллер бота для модерации комментариев"""
def __init__(self):
if not BOT_TOKEN:
raise ValueError("BOT_TOKEN не задан в .env")
self.bot = Bot(token=BOT_TOKEN)
self.dp = Dispatcher()
self.editing_comments: dict[int, dict] = {} # chat_id: {message_id, comment_data}
self._register_handlers()
def _register_handlers(self):
"""Регистрация обработчиков"""
self.dp.message(CommandStart())(self.cmd_start)
self.dp.message(Command("stats"))(self.cmd_stats)
self.dp.message(Command("pending"))(self.cmd_pending)
self.dp.message(Command("sessions"))(self.cmd_sessions)
self.dp.message(Command("groups"))(self.cmd_groups)
self.dp.message(Command("add_group"))(self.cmd_add_group)
self.dp.message(Command("help"))(self.cmd_help)
self.dp.message(F.text)(self.handle_text_message)
self.dp.callback_query()(self.handle_callback)
# Состояния для добавления группы
self.add_group_state: dict = {} # user_id: {'waiting': True}
async def cmd_start(self, message: Message):
"""Обработчик команды /start"""
if not self._is_admin(message.from_user.id):
await message.answer("У вас нет доступа к управлению ботом")
return
# Получаем список групп
groups = get_target_groups()
groups_text = "\n".join([f"• `{g['group_id']}`" for g in groups]) if groups else "Нет добавленных групп"
await message.answer(
f"🤖 **Bot Controller**\n\n"
f"📋 Группы:\n{groups_text}\n\n"
f"Используйте меню для управления:",
reply_markup=create_main_keyboard())
async def cmd_stats(self, message: Message):
"""Обработчик команды /stats"""
if not self._is_admin(message.from_user.id):
return
summary = get_summary_stats()
sessions = get_active_sessions()
text = (
"📊 **Статистика**\n\n"
f"📝 Всего комментариев: `{summary.get('total_comments', 0)}`\n"
f"⏳ Ожидают: `{summary.get('pending', 0)}`\n"
f"✅ Одобрено: `{summary.get('approved', 0)}`\n"
f"❌ Отклонено: `{summary.get('rejected', 0)}`\n"
f"📤 Отправлено: `{summary.get('sent', 0)}`\n\n"
f"👥 Активных сессий: `{len(sessions)}`"
)
await message.answer(text)
async def cmd_pending(self, message: Message):
"""Обработчик команды /pending"""
if not self._is_admin(message.from_user.id):
return
pending = get_pending_comments()
if not pending:
await message.answer("✅ Нет ожидающих комментариев")
return
await message.answer(f"📝 Ожидают модерации: {len(pending)}")
# Показываем первые 5
for comment in pending[:5]:
await self._send_moderation_message(
chat_id=message.chat.id,
comment=comment
)
async def cmd_sessions(self, message: Message):
"""Обработчик команды /sessions"""
if not self._is_admin(message.from_user.id):
return
sessions = get_all_sessions()
if not sessions:
await message.answer(
"❌ Нет активных сессий\n\n"
"Добавьте файлы сессий в папку `sessions/`"
)
return
text = "👥 **Сессии**\n\n"
for session in sessions:
status = "🟢" if session['is_active'] else "🔴"
username = f"@{session['username']}" if session['username'] else "Без username"
text += f"{status} `{session['session_file']}` — {username}\n"
await message.answer(text)
async def cmd_help(self, message: Message):
"""Обработчик команды /help"""
if not self._is_admin(message.from_user.id):
return
text = (
"📖 Справка\n\n"
"Кнопки меню:\n"
"📊 Статистика\n"
"👥 Сессии\n"
"📝 Ожидающие\n"
"📋 Группы\n"
" Добавить группу\n"
"❓ Помощь\n\n"
"Команды:\n"
"/start - Главное меню\n"
"/stats - Статистика\n"
"/pending - Ожидающие\n"
"/groups - Группы\n"
"/add_group ID - Добавить группу\n"
"/help - Справка"
)
await message.answer(text, reply_markup=create_main_keyboard())
async def handle_text_message(self, message: Message):
"""Обработчик текстовых сообщений (кнопки меню)"""
if not self._is_admin(message.from_user.id):
return
text = message.text.strip()
if text == "📊 Статистика":
await self.cmd_stats(message)
elif text == "👥 Сессии":
await self.cmd_sessions(message)
elif text == "📝 Ожидающие":
await self.cmd_pending(message)
elif text == "📋 Группы":
await self.cmd_groups(message)
elif text == " Добавить группу":
await message.answer(
"📝 Добавление группы\n\n"
"Отправьте ID группы или username:\n\n"
"Примеры:\n"
"@telegram (публичный канал)\n"
"1234567890 (ID)\n"
"-1001234567890 (ID канала)"
)
# Включаем состояние ожидания
self.add_group_state[message.from_user.id] = {'waiting': True}
elif text == "❓ Помощь":
await self.cmd_help(message)
else:
# Проверяем, не ждём ли мы ввод
if message.from_user.id in self.add_group_state:
# Пользователь отправил ID или username
await self._add_group_by_input(message, text)
del self.add_group_state[message.from_user.id]
else:
await message.answer(
"Неизвестная команда. Используйте меню:",
reply_markup=create_main_keyboard()
)
async def _add_group_by_username(self, message: Message, username: str):
"""Добавление группы по username"""
# Пробуем вступить всеми сессиями
join_results = await session_manager.join_all_sessions(username)
# Проверяем результаты
all_success = all('' in result for result in join_results.values())
if not all_success:
results_text = "\n".join([f"{k}: {v}" for k, v in join_results.items()])
await message.answer(
f"❌ Ошибка при вступлении в группу {username}\n\n"
f"Возможно группа приватная или не существует.\n\n"
f"Результаты:\n{results_text}"
)
return
# Получаем информацию
group_name = None
try:
chat = await self.bot.get_chat(username)
group_name = chat.title
except:
pass
# Добавляем в БД (username без @)
clean_username = username.lstrip('@')
if add_target_group(clean_username, group_name, clean_username):
await message.answer(
f"✅ Группа {group_name or username} добавлена!\n\n"
f"Username: {username}\n"
f"Все сессии вступили.\n"
f"Бот будет мониторить сообщения."
)
else:
await message.answer(f"❌ Ошибка при добавлении группы {username}")
async def _add_group_by_id(self, message: Message, group_id: int):
"""Добавление группы по ID с авто-вступлением"""
# Конвертируем ID в правильный формат для Telethon
# Если ID начинается с 100, добавляем -100 префикс
if str(group_id).startswith('100') and not str(group_id).startswith('-'):
telethon_id = -int(f"100{group_id}")
elif str(group_id).startswith('-100'):
telethon_id = int(group_id)
else:
telethon_id = int(group_id)
# Пробуем вступить всеми сессиями
join_results = await session_manager.join_all_sessions(telethon_id)
# Проверяем результаты
all_success = all('' in result for result in join_results.values())
if not all_success:
results_text = "\n".join([f"{k}: {v}" for k, v in join_results.items()])
await message.answer(
f"❌ Ошибка при вступлении в группу {group_id}\n\n"
f"Возможно группа приватная.\n\n"
f"Результаты:\n{results_text}\n\n"
f"Используйте публичные группы/каналы."
)
return
# Все сессии вступили — получаем информацию через Telethon (не через Bot API)
group_name = None
group_username = None
try:
# Используем сессию для получения информации
clients = session_manager.clients
if clients:
first_client = list(clients.values())[0]
entity = await first_client.get_entity(telethon_id)
group_name = getattr(entity, 'title', None)
group_username = getattr(entity, 'username', None)
except Exception as e:
logger.warning(f"Не удалось получить информацию о группе {group_id}: {e}")
# Добавляем группу в БД
if add_target_group(telethon_id, group_name, group_username):
username_text = f"@{group_username}" if group_username else "Нет"
await message.answer(
f"✅ Группа {group_name or group_id} добавлена!\n\n"
f"ID: {group_id}\n"
f"Username: {username_text}\n"
f"Все сессии вступили в группу.\n"
f"Теперь бот будет мониторить сообщения."
)
else:
await message.answer(f"❌ Ошибка при добавлении группы {group_id}")
async def cmd_groups(self, message: Message):
"""Обработчик команды /groups"""
if not self._is_admin(message.from_user.id):
return
groups = get_all_target_groups()
if not groups:
await message.answer(
"📋 **Нет добавленных групп**\n\n"
"Добавьте группу командой:\n"
"`/add_group ID_группы`\n\n"
"Или перешлите сообщение из канала, который хотите добавить.")
return
text = "📋 **Целевые группы**\n\n"
for group in groups:
status = "🟢" if group['is_active'] else "🔴"
name = group['group_name'] or f"Группа {group['group_id']}"
text += f"{status} `{name}` (ID: `{group['group_id']}`)\n"
await message.answer(
text,
reply_markup=create_groups_list_keyboard(groups)
)
async def cmd_add_group(self, message: Message):
"""Обработчик команды /add_group"""
if not self._is_admin(message.from_user.id):
return
# Проверяем, есть ли аргументы (ID группы или username)
args = message.text.split()
if len(args) > 1:
# ID или username передан в команде
group_input = args[1]
await self._add_group_by_input(message, group_input)
else:
# Нет аргументов - просим отправить ID
await message.answer(
"📝 Добавление группы\n\n"
"Отправьте ID группы или username:\n\n"
"Примеры:\n"
"@telegram (публичный канал)\n"
"1234567890 (ID)\n"
"-1001234567890 (ID канала)"
)
self.add_group_state[message.from_user.id] = {'waiting': True}
async def _add_group_by_input(self, message: Message, group_input: str):
"""Добавление группы по ID или username"""
# Определяем формат ввода
if group_input.startswith('@'):
# Username - используем как есть
await self._add_group_by_username(message, group_input)
else:
# Числовой ID
try:
group_id = int(group_input)
await self._add_group_by_id(message, group_id)
except ValueError:
await message.answer(
"❌ Неверный формат.\n\n"
"Примеры:\n"
"@telegram (username)\n"
"1234567890 (ID)\n"
"-1001234567890 (ID канала)"
)
async def handle_new_post(self, message: Message):
"""Обработка новых постов (если бот добавлен в канал)"""
# Эта функция для будущего расширения
# Сейчас посты обрабатываются через scan_previous_messages
pass
async def handle_callback(self, callback: CallbackQuery):
"""Обработчик callback-запросов"""
if not self._is_admin(callback.from_user.id):
await callback.answer("❌ Нет доступа", show_alert=True)
return
data = callback.data
logger.info(f"Callback: {data}")
try:
if data == "stats":
await self._callback_stats(callback)
elif data == "sessions":
await self._callback_sessions_list(callback)
elif data.startswith("session_select:"):
session_file = data.split(":")[1]
await self._callback_session_pending(callback, session_file)
elif data == "pending":
await self._callback_pending_groups(callback)
elif data.startswith("group_pending:"):
group_id = data.split(":")[1]
await self._callback_group_pending(callback, group_id)
elif data == "groups":
await self._callback_groups(callback)
elif data == "group_add":
await self._callback_group_add(callback)
elif data.startswith("group_info:"):
await self._callback_group_info(callback, data)
elif data.startswith("group_pause:"):
await self._callback_group_action(callback, "pause", data)
elif data.startswith("group_resume:"):
await self._callback_group_action(callback, "resume", data)
elif data.startswith("group_delete:"):
await self._callback_group_action(callback, "delete", data)
elif data.startswith("approve:"):
await self._callback_approve(callback)
elif data.startswith("reject:"):
await self._callback_reject(callback)
elif data.startswith("regenerate:"):
await self._callback_regenerate(callback)
elif data.startswith("edit:"):
await self._callback_edit(callback)
elif data.startswith("edit_cancel:"):
await self._callback_edit_cancel(callback)
elif data.startswith("session_status:"):
await self._callback_session_action(callback, "status", data)
elif data.startswith("session_pause:"):
await self._callback_session_action(callback, "pause", data)
elif data.startswith("session_resume:"):
await self._callback_session_action(callback, "resume", data)
elif data.startswith("session_delete:"):
await self._callback_session_action(callback, "delete", data)
elif data == "main_menu":
await self._callback_main_menu(callback)
else:
await callback.answer("Неизвестная команда")
except Exception as e:
logger.error(f"Ошибка при обработке callback {data}: {e}")
await callback.answer(f"❌ Ошибка: {e}", show_alert=True)
async def _callback_stats(self, callback: CallbackQuery):
"""Callback для статистики"""
summary = get_summary_stats()
text = (
"📊 **Статистика**\n\n"
f"📝 Всего: `{summary.get('total_comments', 0)}`\n"
f"⏳ Ожидают: `{summary.get('pending', 0)}`\n"
f"✅ Одобрено: `{summary.get('approved', 0)}`\n"
f"❌ Отклонено: `{summary.get('rejected', 0)}`\n"
f"📤 Отправлено: `{summary.get('sent', 0)}`"
)
await callback.message.edit_text(
text,
reply_markup=create_main_menu()
)
await callback.answer()
async def _callback_sessions(self, callback: CallbackQuery):
"""Callback для сессий"""
sessions = get_all_sessions()
if not sessions:
text = "❌ Нет сессий\n\nДобавьте файлы в `sessions/`"
else:
text = "👥 **Сессии**\n\n"
for session in sessions:
status = "🟢" if session['is_active'] else "🔴"
username = f"@{session['username']}" if session['username'] else "Без username"
text += f"{status} `{session['session_file']}` — {username}\n"
await callback.message.edit_text(text)
await callback.answer()
async def _callback_pending(self, callback: CallbackQuery):
"""Callback для ожидающих комментариев"""
pending = get_pending_comments()
if not pending:
await callback.answer("✅ Нет ожидающих", show_alert=True)
return
await callback.message.edit_text(f"📝 Ожидают: {len(pending)}")
for comment in pending[:10]:
await self._send_moderation_message(
chat_id=callback.message.chat.id,
comment=comment
)
await callback.answer()
async def _callback_sessions_list(self, callback: CallbackQuery):
"""Callback для списка сессий"""
sessions = get_all_sessions()
if not sessions:
text = "❌ Нет сессий\n\nДобавьте файлы сессий в `sessions/`"
await callback.message.edit_text(text, reply_markup=create_back_keyboard())
else:
text = "👥 **Сессии**\n\nВыберите сессию для просмотра комментариев:\n\n"
for session in sessions:
status = "🟢" if session['is_active'] else "🔴"
username = f"@{session['username']}" if session.get('username') else ""
text += f"{status} `{session['session_file']}` {username}\n"
await callback.message.edit_text(
text,
parse_mode="Markdown",
reply_markup=create_sessions_list_keyboard(sessions)
)
await callback.answer()
async def _callback_session_pending(self, callback: CallbackQuery, session_file: str):
"""Callback для просмотра pending комментариев сессии"""
pending = get_pending_comments_by_session(session_file)
if not pending:
await callback.message.edit_text(
f"✅ Нет ожидающих комментариев для `{session_file}`",
reply_markup=create_back_keyboard()
)
else:
await callback.message.edit_text(
f"📝 Ожидают модерации ({len(pending)}):\n\nСессия: `{session_file}`",
reply_markup=create_back_keyboard()
)
for comment in pending[:10]:
await self._send_moderation_message(
chat_id=callback.message.chat.id,
comment=comment
)
await callback.answer()
async def _callback_pending_groups(self, callback: CallbackQuery):
"""Callback для выбора группы из pending"""
groups = get_all_target_groups()
if not groups:
text = "📋 **Нет групп**\n\nДобавьте группу через /add_group"
await callback.message.edit_text(text, parse_mode="Markdown", reply_markup=create_back_keyboard())
else:
text = "📋 **Группы**\n\nВыберите группу для просмотра комментариев:\n\n"
for group in groups:
name = group['group_name'] or f"Группа {group['group_id']}"
text += f"📢 `{name}`\n"
await callback.message.edit_text(
text,
parse_mode="Markdown",
reply_markup=create_groups_list_for_pending_keyboard(groups)
)
await callback.answer()
async def _callback_group_pending(self, callback: CallbackQuery, group_id: str):
"""Callback для просмотра pending комментариев группы"""
pending = get_pending_comments_by_group(group_id)
if not pending:
await callback.message.edit_text(
f"✅ Нет ожидающих комментариев для этой группы",
reply_markup=create_back_keyboard()
)
else:
await callback.message.edit_text(
f"📝 Ожидают модерации ({len(pending)}):\n\nГруппа: `{group_id}`",
reply_markup=create_back_keyboard()
)
for comment in pending[:10]:
await self._send_moderation_message(
chat_id=callback.message.chat.id,
comment=comment
)
await callback.answer()
async def _callback_main_menu(self, callback: CallbackQuery):
"""Callback для возврата в главное меню"""
await self.cmd_start(callback.message)
await callback.answer()
async def _callback_settings(self, callback: CallbackQuery):
"""Callback для настроек"""
text = (
"⚙️ **Настройки**\n\n"
f"Log Group: `{LOG_GROUP_ID}`\n"
f"Scan Limit: `{INITIAL_SCAN_LIMIT}`"
)
await callback.message.edit_text(text)
await callback.answer()
async def _callback_groups(self, callback: CallbackQuery):
"""Callback для списка групп"""
groups = get_all_target_groups()
if not groups:
text = "📋 **Нет добавленных групп**\n\n"
text += "Добавьте группу командой `/add_group ID`"
else:
text = "📋 **Целевые группы**\n\n"
for group in groups:
status = "🟢" if group['is_active'] else "🔴"
name = group['group_name'] or f"Группа {group['group_id']}"
username = f"@{group['group_username']}" if group['group_username'] else ""
text += f"{status} **{name}** {username}\n(ID: `{group['group_id']}`)\n"
await callback.message.edit_text(
text,
reply_markup=create_groups_list_keyboard(groups)
)
await callback.answer()
async def _callback_group_add(self, callback: CallbackQuery):
"""Callback для добавления группы"""
await callback.message.answer(
"📝 **Добавление группы**\n\n"
"Отправьте ID группы или перешлите сообщение из канала.\n\n"
"Пример: `/add_group 123456789`")
await callback.answer()
async def _callback_group_action(self, callback: CallbackQuery):
"""Callback для действий с группой"""
data = callback.data
parts = data.split(":")
if len(parts) < 2:
await callback.answer("❌ Ошибка данных", show_alert=True)
return
action = parts[0]
group_id = parts[1] # Может быть username или ID
if action == "group_info":
# Показываем информацию о группе
groups = get_all_target_groups()
group = next((g for g in groups if str(g['group_id']) == str(group_id)), None)
if group:
# Экранируем символы для Markdown
name = (group['group_name'] or 'Нет').replace('_', ' ')
username = group['group_username'] or 'Нет'
status = '🟢 Активна' if group['is_active'] else '🔴 Неактивна'
text = (
f"📋 Группа\n\n"
f"ID: {group['group_id']}\n"
f"Название: {name}\n"
f"Username: @{username}\n"
f"Статус: {status}"
)
await callback.message.edit_text(
text,
reply_markup=create_group_action_keyboard(group['group_id'])
)
else:
await callback.answer("❌ Группа не найдена", show_alert=True)
elif action == "group_pause":
toggle_target_group(group_id, False)
await callback.message.answer(f"⏸️ Группа {group_id} приостановлена")
await self._callback_groups(callback)
elif action == "group_resume":
toggle_target_group(group_id, True)
await callback.message.answer(f"▶️ Группа {group_id} активирована")
await self._callback_groups(callback)
elif action == "group_delete":
# Получаем информацию о группе перед удалением
groups = get_all_target_groups()
group = next((g for g in groups if str(g['group_id']) == str(group_id)), None)
if group:
group_db_id = group['group_id'] # ID как записан в БД
comments_group_id = group.get('comments_group_id')
# Выходим из группы во всех сессиях
try:
leave_results = await session_manager.leave_all_sessions(group_db_id)
logger.info(f"Выход из группы {group_db_id}: {leave_results}")
except Exception as e:
logger.warning(f"Не удалось выйти из группы {group_db_id}: {e}")
# Удаляем все комментарии этой группы из БД
conn = None
try:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Собираем все возможные chat_id для этой группы
chat_ids_to_delete = set()
# Добавляем group_id в разных форматах
chat_ids_to_delete.add(str(group_db_id))
try:
chat_ids_to_delete.add(str(abs(int(group_db_id))))
chat_ids_to_delete.add(str(-abs(int(group_db_id))))
except (ValueError, TypeError):
pass # group_id не числовое (username)
# Добавляем comments_group_id если есть
if comments_group_id:
chat_ids_to_delete.add(str(comments_group_id))
try:
chat_ids_to_delete.add(str(abs(int(comments_group_id))))
chat_ids_to_delete.add(str(-abs(int(comments_group_id))))
except (ValueError, TypeError):
pass
# Удаляем комментарии по всем chat_id
for chat_id in chat_ids_to_delete:
try:
cursor.execute('DELETE FROM comments WHERE chat_id = ?', (chat_id,))
deleted = cursor.rowcount
if deleted > 0:
logger.info(f"Удалено {deleted} комментариев с chat_id={chat_id}")
except Exception as e:
logger.debug(f"Ошибка при удалении chat_id={chat_id}: {e}")
conn.commit()
logger.info(f"Удалено комментариев для группы {group_db_id}")
except Exception as e:
logger.error(f"Ошибка при удалении комментариев: {e}")
finally:
if conn:
conn.close()
# Удаляем группу из БД
remove_target_group(group['group_id'])
await callback.message.answer(f"🗑️ Группа {group['group_id']} удалена")
await self._callback_groups(callback)
await callback.answer()
async def _send_moderation_message(self, chat_id: int, comment: dict):
"""Отправка сообщения на модерацию"""
# Используем channel_id из БД (сохранён при генерации)
channel_id = comment.get('channel_id') or comment['chat_id']
post_url = f"https://t.me/c/{channel_id}/{comment['message_id']}"
# Получаем название группы/канала из БД
# Ищем по chat_id (группа комментариев) или по channel_id (канал)
groups = get_all_target_groups()
group = None
# Сначала ищем по chat_id (группа комментариев)
for g in groups:
if str(g['group_id']) == str(comment['chat_id']):
group = g
break
# Или по channel_id (канал)
if str(g.get('group_id')) == str(channel_id):
group = g
break
# Или по comments_group_id
if str(g.get('comments_group_id')) == str(comment['chat_id']):
group = g
break
group_name = group['group_name'] if group and group.get('group_name') else f"Канал {channel_id}"
session_info = ""
if comment.get('session_file'):
session_info = f"👤 Сессия: `{comment['session_file']}`\n\n"
post_text = comment.get('post_text', 'Текст поста не сохранён')
if len(post_text) > 300:
post_text = post_text[:300] + "..."
text = (
f"📝 Новый комментарий\n\n"
f"📢 Канал: **{group_name}**\n"
f"🔗 Пост: {post_url}\n"
f"{session_info}"
f"📄 Текст поста:\n"
f"_{post_text}_\n\n"
f"💬 Комментарий:\n"
f"_{comment['comment_text'][:500]}_{'...' if len(comment['comment_text']) > 500 else ''}\n\n"
f"🔄 Регенераций: {comment.get('regenerations', 0)}"
)
keyboard = create_moderation_keyboard(
comment['id'],
comment['message_id'],
comment['chat_id']
)
await self.bot.send_message(
chat_id=chat_id,
text=text,
reply_markup=keyboard
)
async def _callback_approve(self, callback: CallbackQuery):
"""Одобрение комментария"""
comment_id = self._parse_callback_data(callback.data)
if not comment_id:
await callback.answer("❌ Ошибка данных", show_alert=True)
return
# Обновляем статус
update_comment_status(comment_id, 'approved')
# Обновляем сообщение
await callback.message.edit_text(
callback.message.text + "\n\n✅ Одобрено")
# Обновляем статистику
update_stats(datetime.now().strftime('%Y-%m-%d'), '', 'approved')
await callback.answer("✅ Одобрено")
async def _callback_reject(self, callback: CallbackQuery):
"""Отклонение комментария"""
comment_id = self._parse_callback_data(callback.data)
if not comment_id:
await callback.answer("❌ Ошибка данных", show_alert=True)
return
update_comment_status(comment_id, 'rejected')
await callback.message.edit_text(
callback.message.text + "\n\n❌ Отклонено")
update_stats(datetime.now().strftime('%Y-%m-%d'), '', 'rejected')
await callback.answer("❌ Отклонено")
def _get_post_url(self, comment: dict) -> str:
"""Получение URL поста"""
channel_id = comment.get('channel_id') or comment['chat_id']
return f"https://t.me/c/{channel_id}/{comment['message_id']}"
async def _callback_regenerate(self, callback: CallbackQuery):
"""Регенерация комментария"""
comment_id = self._parse_callback_data(callback.data)
if not comment_id:
await callback.answer("❌ Ошибка данных", show_alert=True)
return
# Получаем комментарий из БД
conn = None
try:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT * FROM comments WHERE id = ?', (comment_id,))
comment = dict(cursor.fetchone())
finally:
if conn:
conn.close()
if not comment:
await callback.answer("❌ Комментарий не найден", show_alert=True)
return
# Проверяем лимит регенераций
if comment.get('regenerations', 0) >= 3:
await callback.answer("⚠️ Достигнут лимит регенераций", show_alert=True)
return
# Генерируем новый комментарий
post_text = comment.get('post_text', '')
if not post_text:
post_text = comment['comment_text'][:200] + "..."
new_comment = await generate_comment(post_text)
if not new_comment:
await callback.answer("❌ Ошибка генерации", show_alert=True)
return
# Сохраняем новый комментарий
save_comment(
comment['message_id'],
comment['chat_id'],
new_comment,
comment.get('session_file'),
comment.get('post_text'),
comment.get('channel_id')
)
increment_regeneration(comment_id)
update_comment_status(comment_id, 'pending')
# Обновляем сообщение
post_url = self._get_post_url(comment)
post_text = comment.get('post_text', 'Текст поста не сохранён')
if len(post_text) > 300:
post_text = post_text[:300] + "..."
text = (
f"📝 Новый комментарий\n\n"
f"🔗 Пост: {post_url}\n"
f"📄 Текст поста:\n"
f"_{post_text}_\n\n"
f"💬 Комментарий:\n"
f"_{new_comment[:500]}_{'...' if len(new_comment) > 500 else ''}\n\n"
f"🔄 Регенераций: {comment.get('regenerations', 0) + 1}"
)
keyboard = create_moderation_keyboard(
comment_id,
comment['message_id'],
comment['chat_id']
)
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer("🔄 Сгенерирован новый вариант")
async def _callback_edit(self, callback: CallbackQuery):
"""Редактирование комментария"""
comment_id = self._parse_callback_data(callback.data)
if not comment_id:
await callback.answer("❌ Ошибка данных", show_alert=True)
return
# Сохраняем состояние редактирования
self.editing_comments[callback.message.chat.id] = {
'comment_id': comment_id,
'mod_message_id': callback.message.message_id
}
keyboard = create_edit_cancel_keyboard(comment_id)
await callback.message.edit_text(
callback.message.text + "\n\n✏️ **Отправьте новый текст комментария**",
reply_markup=keyboard
)
await callback.answer("✏️ Отправьте новый текст")
async def _callback_edit_cancel(self, callback: CallbackQuery):
"""Отмена редактирования"""
parts = callback.data.split(':')
comment_id = int(parts[1])
# Получаем оригинальный комментарий
conn = None
try:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('SELECT * FROM comments WHERE id = ?', (comment_id,))
comment = dict(cursor.fetchone())
finally:
if conn:
conn.close()
if comment:
post_url = self._get_post_url(comment)
text = (
f"📝 Новый комментарий\n\n"
f"🔗 Пост: {post_url}\n"
f"💬 Текст:\n"
f"_{comment['comment_text'][:500]}_"
)
keyboard = create_moderation_keyboard(
comment_id,
comment['message_id'],
comment['chat_id']
)
await callback.message.edit_text(text, reply_markup=keyboard)
# Удаляем из editing_comments
if callback.message.chat.id in self.editing_comments:
del self.editing_comments[callback.message.chat.id]
await callback.answer("✏️ Редактирование отменено")
async def _callback_session_action(self, callback: CallbackQuery):
"""Действия с сессией"""
action, session_file = callback.data.split(':', 1)
if action == "session_status":
session = next((s for s in get_all_sessions() if s['session_file'] == session_file), None)
if session:
status = "🟢 Активна" if session['is_active'] else "🔴 Неактивна"
await callback.answer(f"{status}", show_alert=True)
elif action == "session_pause":
toggle_session(session_file, False)
await callback.answer("⏸️ Сессия приостановлена")
elif action == "session_resume":
toggle_session(session_file, True)
await callback.answer("▶️ Сессия активирована")
elif action == "session_delete":
delete_session(session_file)
await callback.message.edit_text("🗑️ Сессия удалена")
await callback.answer()
def _parse_callback_data(self, data: str) -> int:
"""Разбор callback данных (возвращает только comment_id)"""
parts = data.split(':')[1:] # Пропускаем действие
if len(parts) >= 1:
try:
return int(parts[0])
except (ValueError, OverflowError):
return None
return None
def _is_admin(self, user_id: int) -> bool:
"""Проверка прав администратора"""
return user_id in ADMIN_IDS or not ADMIN_IDS
async def start(self):
"""Запуск бота"""
logger.info("Запуск Bot Controller...")
# Инициализация БД
init_db()
# Загрузка сессий
await session_manager.create_all_clients()
# Запуск polling
await self.dp.start_polling(self.bot)
async def stop(self):
"""Остановка бота"""
logger.info("Остановка Bot Controller...")
await session_manager.disconnect_all()
await self.bot.close()
async def main():
"""Точка входа"""
bot = CommentBot()
try:
await bot.start()
except KeyboardInterrupt:
await bot.stop()
if __name__ == '__main__':
asyncio.run(main())