0.0.1 Features: - Multi-account support via session files - AI comments generation via Ollama (local LLM) - Telegram bot for moderation (approve/reject/regenerate) - Docker support (controller + worker) - Auto-join public groups - Comment regeneration on group re-add - Statistics tracking Tech stack: - Python 3.11 - Telethon 1.34 (Telegram user client) - Aiogram 3.4 (Telegram bot framework) - SQLite (Database) - Docker & Docker Compose - Ollama (Local LLM)
910 lines
38 KiB
Python
910 lines
38 KiB
Python
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
|
||
)
|
||
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(callback)
|
||
elif data == "pending":
|
||
await self._callback_pending(callback)
|
||
elif data == "groups":
|
||
await self._callback_groups(callback)
|
||
elif data == "group_add":
|
||
await self._callback_group_add(callback)
|
||
elif data.startswith("group_"):
|
||
await self._callback_group_action(callback)
|
||
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_"):
|
||
await self._callback_session_action(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_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']}"
|
||
|
||
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"🔗 Пост: {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())
|