Initial commit: Batch Bot - Telegram Comment Bot
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)
This commit is contained in:
909
bot/controller.py
Normal file
909
bot/controller.py
Normal file
@@ -0,0 +1,909 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user