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:
1
bot/__init__.py
Normal file
1
bot/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Bot package
|
||||
57
bot/config.py
Normal file
57
bot/config.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Загрузка переменных окружения
|
||||
load_dotenv()
|
||||
|
||||
# Пути
|
||||
BASE_DIR = Path(__file__).parent.parent # Корень проекта (batch-bot/)
|
||||
DATA_DIR = BASE_DIR / "data" # Примонтированная директория для данных
|
||||
SESSIONS_DIR = BASE_DIR / "sessions"
|
||||
LOGS_DIR = DATA_DIR / "logs"
|
||||
DB_PATH = DATA_DIR / "comments.db"
|
||||
|
||||
# Telegram Bot (контроллер)
|
||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||
|
||||
# Telegram API (воркеры)
|
||||
API_ID = int(os.getenv("TELEGRAM_API_ID", "0"))
|
||||
API_HASH = os.getenv("TELEGRAM_API_HASH", "")
|
||||
|
||||
# Группы (добавляются через бота)
|
||||
LOG_GROUP_ID = int(os.getenv("LOG_GROUP_ID", "0"))
|
||||
|
||||
# Ollama
|
||||
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434").rstrip("/")
|
||||
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "qwen3:30b-a3b")
|
||||
PROMPT_FILE = os.getenv("PROMPT_FILE", "prompt.txt")
|
||||
|
||||
# Сканирование
|
||||
INITIAL_SCAN_LIMIT = int(os.getenv("INITIAL_SCAN_LIMIT", "20"))
|
||||
|
||||
# Комментарии
|
||||
MAX_REGENERATIONS = int(os.getenv("MAX_REGENERATIONS", "3"))
|
||||
COMMENT_DELAY_MIN = int(os.getenv("COMMENT_DELAY_MIN", "1"))
|
||||
COMMENT_DELAY_MAX = int(os.getenv("COMMENT_DELAY_MAX", "5"))
|
||||
|
||||
# Админы
|
||||
ADMIN_IDS = [int(x.strip()) for x in os.getenv("ADMIN_IDS", "").split(",") if x.strip()]
|
||||
|
||||
# Проверка конфигурации
|
||||
def validate_config():
|
||||
errors = []
|
||||
|
||||
if not BOT_TOKEN:
|
||||
errors.append("BOT_TOKEN не задан")
|
||||
|
||||
if not API_ID or not API_HASH:
|
||||
errors.append("TELEGRAM_API_ID или TELEGRAM_API_HASH не заданы")
|
||||
|
||||
if not LOG_GROUP_ID:
|
||||
errors.append("LOG_GROUP_ID не задан")
|
||||
|
||||
if not ADMIN_IDS:
|
||||
errors.append("ADMIN_IDS не заданы")
|
||||
|
||||
return errors
|
||||
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())
|
||||
591
bot/db.py
Normal file
591
bot/db.py
Normal file
@@ -0,0 +1,591 @@
|
||||
import sqlite3
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from bot.config import DB_PATH
|
||||
|
||||
logger = logging.getLogger('db')
|
||||
|
||||
|
||||
def get_connection():
|
||||
"""Получение соединения с БД"""
|
||||
# Создаём директорию если не существует
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Инициализация базы данных"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Таблица целевых групп
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS target_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
group_id INTEGER UNIQUE NOT NULL,
|
||||
group_name TEXT,
|
||||
group_username TEXT,
|
||||
group_type TEXT DEFAULT 'channel',
|
||||
comments_group_id INTEGER,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# Таблица комментариев
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
message_id INTEGER NOT NULL,
|
||||
chat_id INTEGER NOT NULL,
|
||||
channel_id INTEGER,
|
||||
post_text TEXT,
|
||||
comment_text TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
regenerations INTEGER DEFAULT 0,
|
||||
session_file TEXT,
|
||||
sent_message_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(message_id, chat_id, session_file)
|
||||
)
|
||||
''')
|
||||
|
||||
# Таблица сессий
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_file TEXT UNIQUE NOT NULL,
|
||||
user_id INTEGER,
|
||||
username TEXT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
phone TEXT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# Таблица статистики
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS stats (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date DATE NOT NULL,
|
||||
session_file TEXT NOT NULL,
|
||||
generated INTEGER DEFAULT 0,
|
||||
approved INTEGER DEFAULT 0,
|
||||
rejected INTEGER DEFAULT 0,
|
||||
sent INTEGER DEFAULT 0,
|
||||
UNIQUE(date, session_file)
|
||||
)
|
||||
''')
|
||||
|
||||
# Индексы
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_comments_status ON comments(status)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_comments_message ON comments(message_id, chat_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_active ON sessions(is_active)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_stats_date ON stats(date)')
|
||||
|
||||
conn.commit()
|
||||
logger.info("База данных успешно инициализирована")
|
||||
return conn
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при инициализации базы данных: {e}")
|
||||
raise
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
# === Комментарии ===
|
||||
|
||||
def save_comment(message_id: int, chat_id: int, comment_text: str, session_file: str = None, post_text: str = None, channel_id: int = None) -> bool:
|
||||
"""Сохранение комментария в базу данных"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO comments (message_id, chat_id, comment_text, session_file, post_text, channel_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(message_id, chat_id, session_file)
|
||||
DO UPDATE SET comment_text = ?, updated_at = CURRENT_TIMESTAMP
|
||||
''', (message_id, chat_id, comment_text, session_file, post_text, channel_id, comment_text))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"Комментарий сохранён: message_id={message_id}, session={session_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении комментария: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_comment(message_id: int, chat_id: int, session_file: str = None) -> dict | None:
|
||||
"""Получение комментария из базы данных"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
if session_file:
|
||||
cursor.execute('''
|
||||
SELECT * FROM comments
|
||||
WHERE message_id = ? AND chat_id = ? AND session_file = ?
|
||||
''', (message_id, chat_id, session_file))
|
||||
else:
|
||||
cursor.execute('''
|
||||
SELECT * FROM comments
|
||||
WHERE message_id = ? AND chat_id = ?
|
||||
''', (message_id, chat_id))
|
||||
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении комментария: {e}")
|
||||
return None
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_comments_for_post(message_id: int, chat_id: int) -> list:
|
||||
"""Получение всех комментариев для поста"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT * FROM comments
|
||||
WHERE message_id = ? AND chat_id = ?
|
||||
ORDER BY session_file
|
||||
''', (message_id, chat_id))
|
||||
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении комментариев: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_comment_status(comment_id: int, status: str) -> bool:
|
||||
"""Обновление статуса комментария"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE comments
|
||||
SET status = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (status, comment_id))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"Статус комментария {comment_id} обновлён на {status}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении статуса: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_comment_sent(comment_id: int, sent_message_id: int) -> bool:
|
||||
"""Обновление ID отправленного комментария"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE comments
|
||||
SET sent_message_id = ?, status = 'sent', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (sent_message_id, comment_id))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении отправленного комментария: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def increment_regeneration(comment_id: int) -> bool:
|
||||
"""Увеличение счётчика регенераций"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE comments
|
||||
SET regenerations = regenerations + 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (comment_id,))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при увеличении счётчика регенераций: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_pending_comments() -> list:
|
||||
"""Получение всех ожидающих комментариев"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT * FROM comments WHERE status = 'pending'
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении ожидающих комментариев: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
# === Сессии ===
|
||||
|
||||
def save_session(session_file: str, user_info: dict) -> bool:
|
||||
"""Сохранение информации о сессии"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO sessions (session_file, user_id, username, first_name, last_name, phone)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(session_file)
|
||||
DO UPDATE SET
|
||||
user_id = ?, username = ?, first_name = ?, last_name = ?, phone = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (
|
||||
session_file, user_info.get('user_id'), user_info.get('username'),
|
||||
user_info.get('first_name'), user_info.get('last_name'), user_info.get('phone'),
|
||||
user_info.get('user_id'), user_info.get('username'),
|
||||
user_info.get('first_name'), user_info.get('last_name'), user_info.get('phone')
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"Сессия сохранена: {session_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении сессии: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_all_sessions() -> list:
|
||||
"""Получение всех сессий"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT * FROM sessions ORDER BY created_at')
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении сессий: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_active_sessions() -> list:
|
||||
"""Получение активных сессий"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT * FROM sessions WHERE is_active = 1 ORDER BY created_at')
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении активных сессий: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def toggle_session(session_file: str, is_active: bool) -> bool:
|
||||
"""Активация/деактивация сессии"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE sessions
|
||||
SET is_active = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE session_file = ?
|
||||
''', (1 if is_active else 0, session_file))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при переключении сессии: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_session(session_file: str) -> bool:
|
||||
"""Удаление сессии"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Удаляем комментарии этой сессии
|
||||
cursor.execute('DELETE FROM comments WHERE session_file = ?', (session_file,))
|
||||
# Удаляем сессию
|
||||
cursor.execute('DELETE FROM sessions WHERE session_file = ?', (session_file,))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сессии: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
# === Статистика ===
|
||||
|
||||
def update_stats(date: str, session_file: str, field: str, value: int = 1) -> bool:
|
||||
"""Обновление статистики"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Проверяем существование записи
|
||||
cursor.execute('''
|
||||
SELECT id FROM stats WHERE date = ? AND session_file = ?
|
||||
''', (date, session_file))
|
||||
|
||||
if cursor.fetchone():
|
||||
cursor.execute(f'''
|
||||
UPDATE stats SET {field} = {field} + ?
|
||||
WHERE date = ? AND session_file = ?
|
||||
''', (value, date, session_file))
|
||||
else:
|
||||
cursor.execute('''
|
||||
INSERT INTO stats (date, session_file, {field})
|
||||
VALUES (?, ?, ?)
|
||||
'''.format(field=field), (date, session_file, value))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении статистики: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_stats(date_from: str = None, date_to: str = None) -> list:
|
||||
"""Получение статистики"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
if date_from and date_to:
|
||||
cursor.execute('''
|
||||
SELECT * FROM stats
|
||||
WHERE date BETWEEN ? AND ?
|
||||
ORDER BY date DESC
|
||||
''', (date_from, date_to))
|
||||
elif date_from:
|
||||
cursor.execute('''
|
||||
SELECT * FROM stats
|
||||
WHERE date >= ?
|
||||
ORDER BY date DESC
|
||||
''', (date_from,))
|
||||
else:
|
||||
cursor.execute('SELECT * FROM stats ORDER BY date DESC LIMIT 30')
|
||||
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_summary_stats() -> dict:
|
||||
"""Получение сводной статистики"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT
|
||||
COUNT(*) as total_comments,
|
||||
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
||||
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved,
|
||||
SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected,
|
||||
SUM(CASE WHEN status = 'sent' THEN 1 ELSE 0 END) as sent
|
||||
FROM comments
|
||||
''')
|
||||
|
||||
row = cursor.fetchone()
|
||||
return dict(row) if row else {}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении сводной статистики: {e}")
|
||||
return {}
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
# === Целевые группы ===
|
||||
|
||||
def add_target_group(group_id: int, group_name: str = None, group_username: str = None, group_type: str = 'channel', comments_group_id: int = None) -> bool:
|
||||
"""Добавление целевой группы"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO target_groups (group_id, group_name, group_username, group_type, comments_group_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(group_id)
|
||||
DO UPDATE SET
|
||||
group_name = COALESCE(?, group_name),
|
||||
group_username = COALESCE(?, group_username),
|
||||
group_type = COALESCE(?, group_type),
|
||||
comments_group_id = COALESCE(?, comments_group_id),
|
||||
is_active = 1,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
''', (group_id, group_name, group_username, group_type, comments_group_id, group_name, group_username, group_type, comments_group_id))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"Группа {group_id} добавлена")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при добавлении группы: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_target_groups() -> list:
|
||||
"""Получение всех активных целевых групп"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT * FROM target_groups WHERE is_active = 1
|
||||
ORDER BY created_at
|
||||
''')
|
||||
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении групп: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_all_target_groups() -> list:
|
||||
"""Получение всех целевых групп"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT * FROM target_groups ORDER BY created_at')
|
||||
return [dict(row) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении групп: {e}")
|
||||
return []
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def remove_target_group(group_id: int) -> bool:
|
||||
"""Удаление целевой группы"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('DELETE FROM target_groups WHERE group_id = ?', (group_id,))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"Группа {group_id} удалена")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении группы: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def toggle_target_group(group_id: int, is_active: bool) -> bool:
|
||||
"""Активация/деактивация группы"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE target_groups
|
||||
SET is_active = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE group_id = ?
|
||||
''', (1 if is_active else 0, group_id))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при переключении группы: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
|
||||
def is_target_group(group_id: int) -> bool:
|
||||
"""Проверка, является ли группа целевой"""
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT 1 FROM target_groups
|
||||
WHERE group_id = ? AND is_active = 1
|
||||
''', (group_id,))
|
||||
|
||||
return cursor.fetchone() is not None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке группы: {e}")
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
120
bot/keyboard.py
Normal file
120
bot/keyboard.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardMarkup, KeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder, ReplyKeyboardBuilder
|
||||
|
||||
|
||||
def create_moderation_keyboard(comment_id: int, message_id: int, chat_id: int) -> InlineKeyboardMarkup:
|
||||
"""Создание клавиатуры модерации для комментария"""
|
||||
# Используем только comment_id для callback data (чтобы не превышать лимит 64 байта)
|
||||
callback_data = f"{comment_id}"
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
# Кнопки одобрения/отклонения
|
||||
builder.button(text="✅ Одобрить", callback_data=f"approve:{callback_data}")
|
||||
builder.button(text="❌ Отклонить", callback_data=f"reject:{callback_data}")
|
||||
builder.button(text="🔄 Регенерировать", callback_data=f"regenerate:{callback_data}")
|
||||
builder.button(text="✏️ Редактировать", callback_data=f"edit:{callback_data}")
|
||||
|
||||
builder.adjust(2, 2)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def create_session_keyboard(session_file: str) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура управления сессией"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.button(text="🔘 Статус", callback_data=f"session_status:{session_file}")
|
||||
builder.button(text="⏸️ Пауза", callback_data=f"session_pause:{session_file}")
|
||||
builder.button(text="▶️ Активировать", callback_data=f"session_resume:{session_file}")
|
||||
builder.button(text="🗑️ Удалить", callback_data=f"session_delete:{session_file}")
|
||||
|
||||
builder.adjust(2, 2)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def create_edit_cancel_keyboard(comment_id: int, message_id: int = 0, chat_id: int = 0) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура для отмены редактирования"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.button(text="❌ Отмена", callback_data=f"edit_cancel:{comment_id}")
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def create_main_menu() -> InlineKeyboardMarkup:
|
||||
"""Главное меню бота (inline)"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.button(text="📊 Статистика", callback_data="stats")
|
||||
builder.button(text="👥 Сессии", callback_data="sessions")
|
||||
builder.button(text="📝 Ожидающие", callback_data="pending")
|
||||
builder.button(text="📋 Группы", callback_data="groups")
|
||||
builder.button(text="⚙️ Настройки", callback_data="settings")
|
||||
|
||||
builder.adjust(2, 2, 1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def create_main_keyboard() -> ReplyKeyboardMarkup:
|
||||
"""Главное меню бота (постоянная клавиатура)"""
|
||||
builder = ReplyKeyboardBuilder()
|
||||
|
||||
builder.button(text="📊 Статистика")
|
||||
builder.button(text="👥 Сессии")
|
||||
builder.button(text="📝 Ожидающие")
|
||||
builder.button(text="📋 Группы")
|
||||
builder.button(text="➕ Добавить группу")
|
||||
builder.button(text="❓ Помощь")
|
||||
|
||||
builder.adjust(2, 2, 2)
|
||||
return builder.as_markup(resize_keyboard=True)
|
||||
|
||||
|
||||
def create_groups_list_keyboard(groups: list) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура со списком групп"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
for group in groups:
|
||||
group_id = group['group_id']
|
||||
status = "🟢" if group['is_active'] else "🔴"
|
||||
name = group['group_name'] or f"Группа {group_id}"
|
||||
builder.button(
|
||||
text=f"{status} {name}",
|
||||
callback_data=f"group_info:{group_id}"
|
||||
)
|
||||
|
||||
builder.adjust(1)
|
||||
|
||||
# Кнопка добавления новой группы
|
||||
builder.button(text="➕ Добавить группу", callback_data="group_add")
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def create_group_action_keyboard(group_id: int) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура действий с группой"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.button(text="⏸️ Пауза", callback_data=f"group_pause:{group_id}")
|
||||
builder.button(text="▶️ Активировать", callback_data=f"group_resume:{group_id}")
|
||||
builder.button(text="🗑️ Удалить", callback_data=f"group_delete:{group_id}")
|
||||
builder.button(text="🔙 Назад", callback_data="groups")
|
||||
|
||||
builder.adjust(2, 2)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def create_sessions_list_keyboard(sessions: list) -> InlineKeyboardMarkup:
|
||||
"""Клавиатура со списком сессий"""
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
for session in sessions:
|
||||
session_file = session['session_file']
|
||||
status = "🟢" if session['is_active'] else "🔴"
|
||||
builder.button(
|
||||
text=f"{status} {session_file}",
|
||||
callback_data=f"session_info:{session_file}"
|
||||
)
|
||||
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
58
bot/ollama.py
Normal file
58
bot/ollama.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import aiohttp
|
||||
from bot.config import OLLAMA_URL, OLLAMA_MODEL, PROMPT_FILE
|
||||
|
||||
logger = logging.getLogger('ollama')
|
||||
|
||||
|
||||
def load_prompt() -> str:
|
||||
"""Загрузка промпта из файла"""
|
||||
try:
|
||||
with open(PROMPT_FILE, 'r', encoding='utf-8') as f:
|
||||
return f.read().strip()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при загрузке промпта: {e}")
|
||||
return "Напиши комментарий к следующему посту:\n\n{text}"
|
||||
|
||||
|
||||
PROMPT_TEMPLATE = load_prompt()
|
||||
|
||||
|
||||
async def generate_comment(text: str, max_retries: int = 3) -> str | None:
|
||||
"""Генерация комментария с помощью Ollama"""
|
||||
prompt = PROMPT_TEMPLATE.format(text=text)
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{OLLAMA_URL}/api/generate",
|
||||
json={
|
||||
"model": OLLAMA_MODEL,
|
||||
"prompt": prompt,
|
||||
"stream": False
|
||||
},
|
||||
timeout=aiohttp.ClientTimeout(total=60)
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
data = await response.json()
|
||||
|
||||
comment = data.get('response', '')
|
||||
# Очистка от тегов
|
||||
comment = comment.replace('<think>', '').replace('</think>', '').strip()
|
||||
|
||||
logger.info(f"Комментарий сгенерирован (попытка {attempt + 1})")
|
||||
return comment
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
logger.warning(f"Ошибка подключения к Ollama (попытка {attempt + 1}): {e}")
|
||||
if attempt < max_retries - 1:
|
||||
await asyncio.sleep(2 ** attempt) # Exponential backoff
|
||||
else:
|
||||
logger.error(f"Не удалось подключиться к Ollama после {max_retries} попыток")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при генерации комментария: {e}")
|
||||
return None
|
||||
|
||||
return None
|
||||
262
bot/session_manager.py
Normal file
262
bot/session_manager.py
Normal file
@@ -0,0 +1,262 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from telethon import TelegramClient
|
||||
from telethon.sessions import StringSession
|
||||
from bot.config import API_ID, API_HASH, SESSIONS_DIR
|
||||
from bot.db import save_session
|
||||
|
||||
logger = logging.getLogger('session_manager')
|
||||
|
||||
|
||||
class SessionManager:
|
||||
"""Менеджер сессий Telegram"""
|
||||
|
||||
def __init__(self):
|
||||
self.sessions_dir = SESSIONS_DIR
|
||||
self.sessions_dir.mkdir(exist_ok=True)
|
||||
self.clients: dict[str, TelegramClient] = {}
|
||||
self.session_info: dict[str, dict] = {}
|
||||
|
||||
def get_session_files(self) -> list[str]:
|
||||
"""Получение списка файлов сессий"""
|
||||
session_files = list(self.sessions_dir.glob("*.session"))
|
||||
return [f.name for f in session_files]
|
||||
|
||||
def load_session_string(self, session_file: str) -> str | None:
|
||||
"""Загрузка строки сессии из файла"""
|
||||
try:
|
||||
session_path = self.sessions_dir / session_file
|
||||
if not session_path.exists():
|
||||
logger.error(f"Файл сессии не найден: {session_file}")
|
||||
return None
|
||||
|
||||
session_string = session_path.read_text(encoding='utf-8').strip()
|
||||
if not session_string:
|
||||
logger.error(f"Файл сессии пуст: {session_file}")
|
||||
return None
|
||||
|
||||
return session_string
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при загрузке сессии {session_file}: {e}")
|
||||
return None
|
||||
|
||||
async def create_client(self, session_file: str) -> TelegramClient | None:
|
||||
"""Создание клиента Telegram для сессии"""
|
||||
try:
|
||||
session_string = self.load_session_string(session_file)
|
||||
if not session_string:
|
||||
return None
|
||||
|
||||
# Проверяем, не подключён ли уже клиент
|
||||
if session_file in self.clients:
|
||||
if self.clients[session_file].is_connected():
|
||||
logger.info(f"Клиент для {session_file} уже подключён")
|
||||
return self.clients[session_file]
|
||||
|
||||
# Создаём новую сессию из строки
|
||||
session = StringSession(session_string)
|
||||
|
||||
client = TelegramClient(
|
||||
session,
|
||||
API_ID,
|
||||
API_HASH,
|
||||
device_model="comment_bot",
|
||||
system_version="Linux",
|
||||
app_version="1.0",
|
||||
lang_code="ru"
|
||||
)
|
||||
|
||||
await client.connect()
|
||||
|
||||
if not await client.is_user_authorized():
|
||||
logger.warning(f"Сессия {session_file} не авторизована")
|
||||
await client.disconnect()
|
||||
return None
|
||||
|
||||
# Получаем информацию о пользователе
|
||||
me = await client.get_me()
|
||||
user_info = {
|
||||
'user_id': me.id,
|
||||
'username': me.username or '',
|
||||
'first_name': me.first_name or '',
|
||||
'last_name': me.last_name or '',
|
||||
'phone': me.phone or ''
|
||||
}
|
||||
|
||||
# Сохраняем информацию в БД
|
||||
save_session(session_file, user_info)
|
||||
|
||||
# Сохраняем клиента и информацию
|
||||
self.clients[session_file] = client
|
||||
self.session_info[session_file] = user_info
|
||||
|
||||
logger.info(f"Клиент подключён: {session_file} (@{user_info['username'] or 'no_username'})")
|
||||
return client
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании клиента для {session_file}: {e}")
|
||||
return None
|
||||
|
||||
async def create_all_clients(self) -> dict[str, TelegramClient]:
|
||||
"""Создание клиентов для всех сессий"""
|
||||
session_files = self.get_session_files()
|
||||
logger.info(f"Найдено сессий: {len(session_files)}")
|
||||
|
||||
tasks = [self.create_client(sf) for sf in session_files]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Фильтруем успешные результаты
|
||||
for session_file, result in zip(session_files, results):
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"Ошибка при подключении {session_file}: {result}")
|
||||
|
||||
return {sf: client for sf, client in zip(session_files, results)
|
||||
if isinstance(client, TelegramClient)}
|
||||
|
||||
async def get_client(self, session_file: str) -> TelegramClient | None:
|
||||
"""Получение клиента для сессии"""
|
||||
if session_file in self.clients and self.clients[session_file].is_connected():
|
||||
return self.clients[session_file]
|
||||
|
||||
return await self.create_client(session_file)
|
||||
|
||||
async def disconnect_client(self, session_file: str):
|
||||
"""Отключение клиента"""
|
||||
if session_file in self.clients:
|
||||
try:
|
||||
await self.clients[session_file].disconnect()
|
||||
logger.info(f"Клиент {session_file} отключён")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отключении клиента {session_file}: {e}")
|
||||
finally:
|
||||
del self.clients[session_file]
|
||||
if session_file in self.session_info:
|
||||
del self.session_info[session_file]
|
||||
|
||||
async def disconnect_all(self):
|
||||
"""Отключение всех клиентов"""
|
||||
tasks = [self.disconnect_client(sf) for sf in list(self.clients.keys())]
|
||||
await asyncio.gather(*tasks)
|
||||
logger.info("Все клиенты отключены")
|
||||
|
||||
def get_session_info(self, session_file: str) -> dict | None:
|
||||
"""Получение информации о сессии"""
|
||||
return self.session_info.get(session_file)
|
||||
|
||||
def get_all_info(self) -> list[dict]:
|
||||
"""Получение информации о всех сессиях"""
|
||||
return [
|
||||
{
|
||||
'session_file': sf,
|
||||
**self.session_info.get(sf, {})
|
||||
}
|
||||
for sf in self.get_session_files()
|
||||
]
|
||||
|
||||
async def join_channel(self, session_file: str, channel_id: int | str) -> tuple[bool, str]:
|
||||
"""Вступление сессии в канал/группу (по ID или username)"""
|
||||
try:
|
||||
client = await self.get_client(session_file)
|
||||
if not client:
|
||||
return False, "Не удалось подключить сессию"
|
||||
|
||||
# Пытаемся получить сущность по ID или username
|
||||
entity = await client.get_entity(channel_id)
|
||||
|
||||
# Проверяем, участник ли уже
|
||||
from telethon.tl.functions.channels import GetParticipantRequest
|
||||
|
||||
try:
|
||||
await client(GetParticipantRequest(entity, await client.get_me()))
|
||||
return True, "Уже участник"
|
||||
except:
|
||||
# Не участник, вступаем
|
||||
pass
|
||||
|
||||
# Вступаем в канал/группу
|
||||
from telethon.tl.functions.channels import JoinChannelRequest
|
||||
|
||||
if hasattr(entity, 'megagroup') or hasattr(entity, 'broadcast'):
|
||||
# Канал или супергруппа
|
||||
await client(JoinChannelRequest(entity))
|
||||
else:
|
||||
# Обычная группа
|
||||
from telethon.tl.functions.messages import AddChatUserRequest
|
||||
await client(AddChatUserRequest(entity.id, 0, fwd_limit=0))
|
||||
|
||||
return True, "Вступил успешно"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "PRIVATE" in error_msg.upper() or "CHANNEL_PRIVATE" in error_msg:
|
||||
return False, "Частный канал/группа"
|
||||
elif "INVITE" in error_msg.upper():
|
||||
return False, "Нужно приглашение"
|
||||
elif "Could not find" in error_msg:
|
||||
return False, "Группа не найдена"
|
||||
else:
|
||||
return False, f"Ошибка: {error_msg[:50]}"
|
||||
|
||||
async def join_all_sessions(self, channel_id: int | str) -> dict[str, str]:
|
||||
"""Все сессии вступают в канал/группу (по ID или username)"""
|
||||
results = {}
|
||||
for session_file in self.get_session_files():
|
||||
success, message = await self.join_channel(session_file, channel_id)
|
||||
results[session_file] = f"{'✅' if success else '❌'} {message}"
|
||||
return results
|
||||
|
||||
async def leave_channel(self, session_file: str, channel_id: int | str) -> tuple[bool, str]:
|
||||
"""Выход сессии из канала/группы"""
|
||||
try:
|
||||
client = await self.get_client(session_file)
|
||||
if not client:
|
||||
return False, "Не удалось подключить сессию"
|
||||
|
||||
# Пробуем разные форматы ID
|
||||
entity = None
|
||||
for try_id in [channel_id, str(channel_id), str(channel_id).lstrip('-')]:
|
||||
try:
|
||||
entity = await client.get_entity(try_id)
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
if not entity:
|
||||
# Если не нашли, пробуем получить из диалогов
|
||||
async for dialog in client.iter_dialogs():
|
||||
if str(dialog.id) == str(channel_id) or str(dialog.id).lstrip('-') == str(channel_id).lstrip('-'):
|
||||
entity = dialog.entity
|
||||
break
|
||||
|
||||
if not entity:
|
||||
return False, "Группа не найдена в диалогах"
|
||||
|
||||
# Выходим из канала/группы
|
||||
from telethon.tl.functions.channels import LeaveChannelRequest
|
||||
await client(LeaveChannelRequest(entity))
|
||||
|
||||
return True, "Вышел успешно"
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "PRIVATE" in error_msg.upper():
|
||||
return False, "Частный канал/группа"
|
||||
elif "BOT" in error_msg.upper() or "ADMIN" in error_msg.upper():
|
||||
return False, "Нельзя выйти (бот/админ)"
|
||||
elif "Cannot find" in error_msg or "not found" in error_msg.lower():
|
||||
return True, "Уже не в группе" # Считаем успешным - уже вышел
|
||||
else:
|
||||
return False, f"Ошибка: {error_msg[:50]}"
|
||||
|
||||
async def leave_all_sessions(self, channel_id: int | str) -> dict[str, str]:
|
||||
"""Все сессии выходят из канала/группы"""
|
||||
results = {}
|
||||
for session_file in self.get_session_files():
|
||||
success, message = await self.leave_channel(session_file, channel_id)
|
||||
results[session_file] = f"{'✅' if success else '❌'} {message}"
|
||||
return results
|
||||
|
||||
|
||||
# Глобальный экземпляр
|
||||
session_manager = SessionManager()
|
||||
405
bot/worker.py
Normal file
405
bot/worker.py
Normal file
@@ -0,0 +1,405 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
from telethon import TelegramClient, events
|
||||
from telethon.tl.types import PeerChannel
|
||||
from bot.config import (
|
||||
API_ID, API_HASH, LOG_GROUP_ID,
|
||||
INITIAL_SCAN_LIMIT, COMMENT_DELAY_MIN, COMMENT_DELAY_MAX
|
||||
)
|
||||
from bot.db import (
|
||||
init_db, save_comment, get_comment, get_comments_for_post,
|
||||
update_comment_status, update_comment_sent, get_active_sessions,
|
||||
update_stats, get_pending_comments, get_target_groups, is_target_group, DB_PATH
|
||||
)
|
||||
from bot.ollama import generate_comment
|
||||
from bot.session_manager import session_manager
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('worker')
|
||||
|
||||
|
||||
class CommentWorker:
|
||||
"""Воркер для отправки комментариев от имени пользователей"""
|
||||
|
||||
def __init__(self):
|
||||
self.clients: dict[str, TelegramClient] = {}
|
||||
self.running = False
|
||||
|
||||
async def start(self):
|
||||
"""Запуск воркера"""
|
||||
logger.info("Запуск Comment Worker...")
|
||||
|
||||
# Инициализация БД
|
||||
init_db()
|
||||
|
||||
# Подключение сессий
|
||||
self.clients = await session_manager.create_all_clients()
|
||||
logger.info(f"Подключено сессий: {len(self.clients)}")
|
||||
|
||||
if not self.clients:
|
||||
logger.warning("Нет активных сессий. Добавьте файлы в sessions/")
|
||||
return
|
||||
|
||||
self.running = True
|
||||
|
||||
# Запускаем задачи параллельно
|
||||
await asyncio.gather(
|
||||
self.listen_for_new_posts(),
|
||||
self.monitor_approved_comments()
|
||||
)
|
||||
|
||||
async def stop(self):
|
||||
"""Остановка воркера"""
|
||||
logger.info("Остановка Comment Worker...")
|
||||
self.running = False
|
||||
await session_manager.disconnect_all()
|
||||
|
||||
async def scan_previous_messages(self):
|
||||
"""Сканирование предыдущих сообщений"""
|
||||
logger.info(f"Сканирование {INITIAL_SCAN_LIMIT} сообщений...")
|
||||
|
||||
# Получаем список целевых групп из БД
|
||||
target_groups = get_target_groups()
|
||||
|
||||
if not target_groups:
|
||||
logger.warning("Нет групп в БД! Добавьте группу через бота: /add_group ID")
|
||||
return
|
||||
|
||||
logger.info(f"Мониторинг групп: {[g['group_id'] for g in target_groups]}")
|
||||
|
||||
for group in target_groups:
|
||||
group_id = group['group_id']
|
||||
|
||||
for session_file, client in self.clients.items():
|
||||
try:
|
||||
target_group = await client.get_entity(PeerChannel(abs(int(group_id))))
|
||||
messages = await client.get_messages(target_group, limit=INITIAL_SCAN_LIMIT)
|
||||
|
||||
logger.info(f"Сессия {session_file}: получено {len(messages)} сообщений из группы {group_id}")
|
||||
|
||||
for message in reversed(messages):
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
await self.process_message(message, session_file, client, group_id)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сканирования для {session_file} в группе {group_id}: {e}")
|
||||
|
||||
async def process_message(self, message, session_file: str, client: TelegramClient, group_id: int = None, comments_group_id: int = None):
|
||||
"""Обработка сообщения"""
|
||||
try:
|
||||
if not message.text or len(message.text) < 10:
|
||||
return
|
||||
|
||||
if not message.replies or not message.replies.comments:
|
||||
logger.info(f"Сообщение {message.id} не поддерживает комментарии")
|
||||
return
|
||||
|
||||
msg_comments_group_id = message.replies.channel_id
|
||||
if not msg_comments_group_id:
|
||||
msg_comments_group_id = comments_group_id
|
||||
|
||||
if not msg_comments_group_id:
|
||||
logger.error(f"Не удалось получить ID группы комментариев для {message.id}")
|
||||
return
|
||||
|
||||
existing = get_comment(message.id, msg_comments_group_id, session_file)
|
||||
if existing:
|
||||
return
|
||||
|
||||
from telethon.tl.functions.channels import GetFullChannelRequest
|
||||
channel_id = None
|
||||
try:
|
||||
comments_group = await client.get_entity(PeerChannel(abs(int(msg_comments_group_id))))
|
||||
channel_full = await client(GetFullChannelRequest(comments_group))
|
||||
if channel_full.full_chat.linked_chat_id:
|
||||
channel_id = abs(int(channel_full.full_chat.linked_chat_id))
|
||||
logger.info(f"Linked chat для {msg_comments_group_id}: {channel_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось получить linked_chat: {e}")
|
||||
channel_id = group_id
|
||||
|
||||
logger.info(f"Генерация комментария для сообщения {message.id} ({session_file})")
|
||||
comment_text = await generate_comment(message.text)
|
||||
|
||||
if not comment_text:
|
||||
logger.error(f"Не удалось сгенерировать комментарий для {message.id}")
|
||||
return
|
||||
|
||||
save_comment(
|
||||
message.id,
|
||||
msg_comments_group_id,
|
||||
comment_text,
|
||||
session_file,
|
||||
message.text,
|
||||
channel_id
|
||||
)
|
||||
|
||||
update_stats(
|
||||
datetime.now().strftime('%Y-%m-%d'),
|
||||
session_file,
|
||||
'generated'
|
||||
)
|
||||
|
||||
logger.info(f"Комментарий сохранён: message_id={message.id}, session={session_file}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки сообщения: {e}")
|
||||
|
||||
async def listen_for_new_posts(self):
|
||||
"""Прослушивание новых постов с авто-обновлением списка групп"""
|
||||
logger.info("Запуск прослушивания новых постов...")
|
||||
|
||||
last_groups_check = 0
|
||||
group_handlers = {}
|
||||
scanned_groups = set()
|
||||
|
||||
while self.running:
|
||||
current_time = datetime.now().timestamp()
|
||||
if current_time - last_groups_check > 10:
|
||||
target_groups = get_target_groups()
|
||||
new_group_ids = [str(g['group_id']) for g in target_groups]
|
||||
|
||||
# Проверяем, есть ли группы которые были удалены и добавлены заново
|
||||
for group_id in new_group_ids:
|
||||
if group_id not in group_handlers:
|
||||
logger.info(f"Добавлена группа для мониторинга: {group_id}")
|
||||
|
||||
# Сканируем последние 10 сообщений новой группы
|
||||
# Всегда сканируем при добавлении, даже если уже было
|
||||
await self.scan_group(group_id, limit=10)
|
||||
scanned_groups.add(group_id)
|
||||
|
||||
for session_file, client in self.clients.items():
|
||||
@client.on(events.NewMessage(chats=group_id))
|
||||
async def handle_new_post(event):
|
||||
message = event.message
|
||||
logger.info(f"Новый пост в группе {group_id}: {message.id}")
|
||||
await self.process_message(message, session_file, client, group_id)
|
||||
|
||||
group_handlers[group_id] = True
|
||||
else:
|
||||
# Группа уже обрабатывается, но проверяем не была ли она удалена и добавлена снова
|
||||
# Если была - очищаем scanned_groups для этой группы
|
||||
if group_id in scanned_groups:
|
||||
# Проверяем что группа всё ещё в БД
|
||||
group_exists = any(str(g['group_id']) == group_id for g in target_groups)
|
||||
if not group_exists:
|
||||
scanned_groups.discard(group_id)
|
||||
del group_handlers[group_id]
|
||||
logger.info(f"Группа {group_id} удалена, сброс кэша")
|
||||
|
||||
# Удаляем обработчики для удалённых групп
|
||||
for group_id in list(group_handlers.keys()):
|
||||
if group_id not in new_group_ids:
|
||||
del group_handlers[group_id]
|
||||
scanned_groups.discard(group_id)
|
||||
logger.info(f"Группа {group_id} удалена из мониторинга")
|
||||
|
||||
last_groups_check = current_time
|
||||
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def scan_group(self, group_id: int | str, limit: int = 10):
|
||||
"""Сканирование последней сообщений из группы/канала"""
|
||||
logger.info(f"Сканирование {limit} сообщений из группы {group_id}...")
|
||||
|
||||
for session_file, client in self.clients.items():
|
||||
try:
|
||||
target_channel = None
|
||||
comments_group_id = None
|
||||
|
||||
try:
|
||||
target_channel = await client.get_entity(group_id)
|
||||
except:
|
||||
async for dialog in client.iter_dialogs():
|
||||
dialog_id = dialog.id
|
||||
dialog_username = getattr(dialog.entity, 'username', None) if hasattr(dialog, 'entity') and dialog.entity else None
|
||||
if str(dialog_id) == str(group_id) or dialog_username == str(group_id).lstrip('@'):
|
||||
target_channel = dialog.entity
|
||||
break
|
||||
|
||||
if not target_channel:
|
||||
logger.error(f"Не удалось найти группу/канал {group_id}")
|
||||
continue
|
||||
|
||||
group_name = getattr(target_channel, 'title', None)
|
||||
group_username = getattr(target_channel, 'username', None)
|
||||
|
||||
from telethon.tl.functions.channels import GetFullChannelRequest
|
||||
try:
|
||||
channel_full = await client(GetFullChannelRequest(target_channel))
|
||||
if hasattr(channel_full.full_chat, 'linked_chat_id') and channel_full.full_chat.linked_chat_id:
|
||||
comments_group_id = channel_full.full_chat.linked_chat_id
|
||||
logger.info(f"Канал {group_id} имеет группу комментариев: {comments_group_id}")
|
||||
|
||||
try:
|
||||
comments_group = await client.get_entity(comments_group_id)
|
||||
if getattr(comments_group, 'username', None):
|
||||
await session_manager.join_channel(session_file, comments_group_id)
|
||||
logger.info(f"Вступил в группу комментариев: {comments_group_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось вступить в группу комментариев: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось получить linked_chat: {e}")
|
||||
|
||||
from bot.db import add_target_group
|
||||
add_target_group(group_id, group_name, group_username, 'channel', comments_group_id)
|
||||
logger.info(f"Обновлена информация: {group_name}, comments_group={comments_group_id}")
|
||||
|
||||
messages = await client.get_messages(target_channel, limit=limit)
|
||||
logger.info(f"Сессия {session_file}: получено {len(messages)} сообщений")
|
||||
|
||||
for message in reversed(messages):
|
||||
if not self.running:
|
||||
break
|
||||
await self.process_message(message, session_file, client, group_id, comments_group_id)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сканирования для {session_file} в группе {group_id}: {e}")
|
||||
|
||||
async def monitor_approved_comments(self):
|
||||
"""Мониторинг одобренных комментариев"""
|
||||
logger.info("Запуск мониторинга одобренных комментариев...")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
conn = None
|
||||
try:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT * FROM comments
|
||||
WHERE status = 'approved' AND sent_message_id IS NULL
|
||||
ORDER BY created_at
|
||||
LIMIT 10
|
||||
''')
|
||||
comments = [dict(row) for row in cursor.fetchall()]
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
if comments:
|
||||
logger.info(f"Найдено {len(comments)} одобренных комментариев для отправки")
|
||||
|
||||
for comment in comments:
|
||||
if not self.running:
|
||||
break
|
||||
|
||||
await self.send_comment(comment)
|
||||
await asyncio.sleep(2)
|
||||
else:
|
||||
logger.debug("Нет комментариев для отправки")
|
||||
|
||||
await asyncio.sleep(10)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка мониторинга: {e}")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
async def send_comment(self, comment: dict):
|
||||
"""Отправка комментария"""
|
||||
session_file = comment.get('session_file')
|
||||
|
||||
if not session_file:
|
||||
logger.error(f"Нет session_file для комментария {comment['id']}")
|
||||
return
|
||||
|
||||
client = self.clients.get(session_file)
|
||||
|
||||
if not client:
|
||||
client = await session_manager.get_client(session_file)
|
||||
if not client:
|
||||
logger.error(f"Не удалось подключить сессию {session_file}")
|
||||
update_comment_status(comment['id'], 'rejected')
|
||||
return
|
||||
|
||||
try:
|
||||
comments_group_id = abs(int(comment['chat_id']))
|
||||
message_id_in_channel = comment['message_id']
|
||||
channel_id = comment.get('channel_id')
|
||||
|
||||
logger.info(f"Отправка комментария {comment['id']}: message_id={message_id_in_channel}, comments_group={comments_group_id}, channel={channel_id}")
|
||||
|
||||
comments_group = await client.get_entity(PeerChannel(comments_group_id))
|
||||
logger.info(f"Группа комментариев: {comments_group.title} (ID: {comments_group_id})")
|
||||
|
||||
from telethon.tl.functions.channels import GetFullChannelRequest
|
||||
channel_full = await client(GetFullChannelRequest(comments_group))
|
||||
linked_chat_id = channel_full.full_chat.linked_chat_id
|
||||
|
||||
if not linked_chat_id:
|
||||
logger.error("Не удалось найти связанный канал")
|
||||
return
|
||||
|
||||
logger.info(f"Linked chat ID: {linked_chat_id}")
|
||||
|
||||
# Получаем сообщение в КАНАЛЕ
|
||||
target_message_in_channel = await client.get_messages(
|
||||
PeerChannel(abs(int(linked_chat_id))),
|
||||
ids=message_id_in_channel
|
||||
)
|
||||
|
||||
if not target_message_in_channel:
|
||||
logger.error(f"Сообщение {message_id_in_channel} не найдено в канале {linked_chat_id}")
|
||||
return
|
||||
|
||||
logger.info(f"Найдено сообщение в канале: {target_message_in_channel.id}")
|
||||
|
||||
# Вступаем в группу комментариев (если ещё не вступили)
|
||||
# Это требуется для некоторых каналов
|
||||
try:
|
||||
await session_manager.join_channel(session_file, comments_group_id)
|
||||
logger.info(f"Вступил в группу комментариев: {comments_group_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось вступить в группу комментариев: {e}")
|
||||
|
||||
# ОТПРАВЛЯЕМ В КАНАЛ с comment_to= (как в старом проекте)
|
||||
# Telegram автоматически направит комментарий в группу комментариев
|
||||
delay = random.uniform(COMMENT_DELAY_MIN, COMMENT_DELAY_MAX)
|
||||
logger.info(f"Задержка {delay:.1f}с...")
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
sent_message = await client.send_message(
|
||||
PeerChannel(abs(int(linked_chat_id))), # КАНАЛ
|
||||
comment['comment_text'],
|
||||
comment_to=target_message_in_channel.id # ID в КАНАЛЕ
|
||||
)
|
||||
logger.info(f"✅ Отправлен в канал как comment_to {target_message_in_channel.id} -> {sent_message.id}")
|
||||
|
||||
logger.info(f"Комментарий отправлен: {comment['id']} -> message {sent_message.id}")
|
||||
|
||||
update_comment_sent(comment['id'], sent_message.id)
|
||||
update_stats(datetime.now().strftime('%Y-%m-%d'), session_file, 'sent')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки комментария: {e}")
|
||||
update_comment_status(comment['id'], 'pending')
|
||||
|
||||
|
||||
async def main():
|
||||
"""Точка входа"""
|
||||
worker = CommentWorker()
|
||||
|
||||
try:
|
||||
await worker.start()
|
||||
except KeyboardInterrupt:
|
||||
await worker.stop()
|
||||
except Exception as e:
|
||||
logger.error(f"Критическая ошибка: {e}")
|
||||
await worker.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user