From bb2716152406656d043d14a7cf83947b5235724a Mon Sep 17 00:00:00 2001 From: bilal Date: Sat, 28 Feb 2026 01:44:40 +0300 Subject: [PATCH] Final release: Multi-session comment bot with filtering Features: - Multi-account support (session files) - AI comments via Ollama - Telegram bot moderation - Filter by sessions and groups - Docker support - Auto-join groups - Log notifications - DB migration script Bug fixes: - Fixed comment_to for proper post targeting - Fixed entity lookup with multiple ID formats - Fixed callback handlers for filtering - Added auto-join before entity lookup --- .gitignore | 5 ++ DEPLOYMENT_CHECKLIST.md | 110 +++++++++++++++++++++++++++ README.md | 36 +++++++-- auth.py | 132 +++++++++++++++++++++------------ bot/controller.py | 160 +++++++++++++++++++++++++++++++++++++--- bot/db.py | 42 +++++++++++ bot/keyboard.py | 31 +++++++- bot/worker.py | 120 ++++++++++++++++++++++-------- 8 files changed, 537 insertions(+), 99 deletions(-) create mode 100644 DEPLOYMENT_CHECKLIST.md diff --git a/.gitignore b/.gitignore index d275eb6..f6f53ac 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,8 @@ logs/ *.swp *.swo *~ + +# macOS .DS_Store # Testing @@ -69,3 +71,6 @@ docs/_build/ tmp/ temp/ *.tmp + +# Migration scripts (optional) +migrate_db.py diff --git a/DEPLOYMENT_CHECKLIST.md b/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..29b80a9 --- /dev/null +++ b/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,110 @@ +# 🚀 Чеклист для отправки в Gitea + +## ✅ Проверено перед отправкой: + +### Файлы проекта: +- [x] `README.md` — обновлённая документация +- [x] `.env.example` — пример конфигурации +- [x] `.gitignore` — игнорирование секретов +- [x] `docker-compose.yml` — Docker конфигурация +- [x] `Dockerfile` — образ контейнера +- [x] `requirements.txt` — Python зависимости +- [x] `auth.py` — автономная авторизация +- [x] `migrate_db.py` — скрипт миграции БД +- [x] `prompt.txt` — шаблон для LLM + +### Исходный код: +- [x] `bot/controller.py` — бот для модерации +- [x] `bot/worker.py` — воркер для отправки +- [x] `bot/db.py` — база данных +- [x] `bot/config.py` — конфигурация +- [x] `bot/keyboard.py` — inline-клавиатуры +- [x] `bot/ollama.py` — Ollama API +- [x] `bot/session_manager.py` — управление сессиями +- [x] `bot/__init__.py` — инициализация пакета + +### НЕ попадает в репозиторий: +- [x] `.env` — секреты +- [x] `sessions/*.session` — сессии +- [x] `data/comments.db` — база данных +- [x] `logs/` — логи +- [x] `__pycache__/` — кэш Python +- [x] `.DS_Store` — системные файлы + +## 📋 Функционал: + +### Основные функции: +- [x] Мультиаккаунт (несколько сессий) +- [x] AI генерация комментариев (Ollama) +- [x] Модерация (approve/reject/regenerate/edit) +- [x] Фильтрация по сессиям +- [x] Фильтрация по группам +- [x] Авто-вступление в группы +- [x] Уведомления в лог-группу +- [x] Удаление групп (выход + сброс) +- [x] Перегенерация при повторном добавлении + +### Docker: +- [x] Controller сервис +- [x] Worker сервис +- [x] Тома для данных +- [x] Сетевая конфигурация + +### Документация: +- [x] README.md (полная) +- [x] .env.example (с комментариями) +- [x] DOCKER.md (Docker инструкция) +- [x] QUICKSTART.md (быстрый старт) +- [x] GIT_INSTRUCTIONS.md (инструкция по Git) + +## 🚀 Команды для отправки: + +```bash +cd /Users/bilal/Documents/code/batch-bot + +# 1. Добавить изменения +git add . + +# 2. Проверить что будет закоммичено +git status + +# 3. Сделать коммит +git commit -m "Final release: Multi-session comment bot with filtering + +Features: +- Multi-account support (session files) +- AI comments via Ollama +- Telegram bot moderation +- Filter by sessions and groups +- Docker support +- Auto-join groups +- Log notifications +- DB migration script + +Bug fixes: +- Fixed comment_to for proper post targeting +- Fixed entity lookup with multiple ID formats +- Fixed callback handlers for filtering +- Added auto-join before entity lookup" + +# 4. Отправить в Gitea +git push origin main +``` + +## ✅ После отправки: + +1. Проверьте репозиторий: https://git.core.com.ru/bilal/batch-bot +2. Убедитесь что все файлы на месте +3. Проверьте что .env и сессии НЕ в репозитории + +## 📊 Статистика проекта: + +- Файлов: ~15 +- Строк кода: ~2500 +- Функций: ~50 +- Callback обработчиков: ~20 +- Таблиц БД: 4 + +--- + +**Готово к отправке!** 🎉 diff --git a/README.md b/README.md index e0bf3a4..61f6d15 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Batch Bot - Telegram Comment Bot -Автоматический бот для генерации и публикации комментариев в Telegram от имени нескольких пользователей. +Автоматический бот для генерации и публикации комментариев в Telegram от имени нескольких пользователей с использованием локальной LLM (Ollama). ## 🏗 Архитектура @@ -30,8 +30,9 @@ - ✅ **Модерация** — inline-кнопки для одобрения/отклонения - ✅ **Редактирование** — возможность изменить текст перед отправкой - ✅ **Статистика** — учёт сгенерированных/отправленных комментариев -- ✅ **Безопасность** — разделение контроллера и воркеров +- ✅ **Фильтрация** — просмотр комментариев по сессиям и группам - ✅ **Docker** — полная контейнеризация +- ✅ **Уведомления** — уведомления о новых постах в лог-группу ## 🚀 Быстрый старт @@ -66,13 +67,19 @@ pip install -r requirements.txt python auth.py ``` -Введите номер телефона и код из Telegram. +Введите API credentials, номер телефона и код из Telegram. **Для нескольких аккаунтов:** - Запустите `python auth.py` несколько раз - Или скопируйте `.session` файлы в `sessions/` -### 3. Запуск Docker +### 3. Миграция БД (если обновляетесь) + +```bash +python migrate_db.py +``` + +### 4. Запуск Docker ```bash # Сборка и запуск @@ -98,12 +105,24 @@ docker-compose down |---------|----------| | `/start` | Главное меню | | `/stats` | Статистика | -| `/pending` | Ожидающие комментарии | -| `/sessions` | Сессии | +| `/pending` | Ожидающие комментарии (по группам) | +| `/sessions` | Сессии (по сессиям) | | `/groups` | Управление группами | | `/add_group ID` | Добавить группу | | `/help` | Справка | +### Фильтрация комментариев + +**По сессиям:** +1. Нажмите "👥 Сессии" +2. Выберите сессию +3. Просмотрите комментарии этой сессии + +**По группам:** +1. Нажмите "📝 Ожидающие" +2. Выберите группу +3. Просмотрите комментарии этой группы + ### Добавление группы **Через команду:** @@ -158,6 +177,7 @@ batch-bot/ ├── Dockerfile # Образ для controller/worker ├── requirements.txt # Python зависимости ├── auth.py # Скрипт авторизации +├── migrate_db.py # Скрипт миграции БД ├── prompt.txt # Шаблон для LLM ├── bot/ │ ├── config.py # Конфигурация @@ -257,6 +277,10 @@ OLLAMA_URL=http://host.docker.internal:11434 - Проверьте что аккаунт вступил в группу комментариев - Worker автоматически вступает при отправке +**"Could not find the input entity":** +- Аккаунт должен быть участником группы +- Worker автоматически вступает при сканировании + ## 📝 Лицензия MIT diff --git a/auth.py b/auth.py index fedfb44..9302fe1 100644 --- a/auth.py +++ b/auth.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 """ -Скрипт для создания сессии Telegram -Запустите этот скрипт для авторизации и создания файла сессии +Telegram Session Creator +Создаёт сессию для бота через интерактивную авторизацию """ import asyncio import os import sys from pathlib import Path -from dotenv import load_dotenv from loguru import logger from telethon import TelegramClient from telethon.sessions import StringSession @@ -27,80 +26,114 @@ logger.add( level="DEBUG" ) -# Загрузка переменных окружения -load_dotenv() - -# Конфигурация -API_ID = int(os.getenv("TELEGRAM_API_ID", "0")) -API_HASH = os.getenv("TELEGRAM_API_HASH", "") -PHONE = os.getenv("TELEGRAM_PHONE", "") - -SESSIONS_DIR = Path("sessions") +# Пути +BASE_DIR = Path(__file__).parent +SESSIONS_DIR = BASE_DIR / "sessions" +SESSIONS_DIR.mkdir(exist_ok=True) async def main(): """Создание сессии""" try: - # Проверка конфигурации - if not all([API_ID, API_HASH]): - logger.error("❌ TELEGRAM_API_ID и TELEGRAM_API_HASH должны быть заданы в .env") + logger.info("=" * 50) + logger.info("🔐 Telegram Session Creator") + logger.info("=" * 50) + logger.info("") + + # Запрос API credentials + logger.info("📋 Для работы нужны API credentials") + logger.info("Получить можно здесь: https://my.telegram.org/apps") + logger.info("") + + api_id = input("Введите API ID: ").strip() + api_hash = input("Введите API Hash: ").strip() + + if not api_id or not api_hash: + logger.error("❌ API ID и API Hash обязательны") return - # Создаём директорию для сессий - SESSIONS_DIR.mkdir(exist_ok=True) + logger.info("") + logger.info("📱 Введите номер телефона") + logger.info("Формат: +79991234567 (с кодом страны)") + logger.info("") - logger.info("🔐 Telegram Session Creator") - logger.info("=" * 40) - logger.info(f"API ID: {API_ID}") - logger.info(f"API Hash: {'*' * len(API_HASH) if API_HASH else 'Не задан'}") - logger.info(f"Phone: {PHONE or 'Будет запрошен'}") - logger.info("=" * 40) + phone = input("Номер телефона: ").strip() - # Запрос телефона если не задан - phone = PHONE if not phone: - phone = input("📱 Введите номер телефона (с +7): ").strip() + logger.error("❌ Номер телефона обязателен") + return # Создаём клиента client = TelegramClient( StringSession(), - API_ID, - API_HASH, - device_model="comment_bot", - system_version="Linux", - app_version="1.0", - lang_code="ru" + int(api_id), + api_hash, + device_model="Desktop (X64)", + system_version="Windows 11", + app_version="3.2.2", + lang_code="en", + system_lang_code="en-US" ) - logger.info("Подключение к Telegram...") + logger.info("") + logger.info("⏳ Подключение к Telegram...") await client.connect() if not await client.is_user_authorized(): - logger.info("Отправка кода подтверждения...") + logger.info("📲 Отправка кода подтверждения...") try: await client.send_code_request(phone) + logger.info(f"✅ Код отправлен на {phone}") except Exception as e: - logger.error(f"Ошибка отправки кода: {e}") + logger.error(f"❌ Ошибка отправки кода: {e}") + await client.disconnect() return # Ввод кода - code = input("📲 Введите код из Telegram: ").strip() + logger.info("") + code = input("Введите код из Telegram: ").strip() + + if not code: + logger.error("❌ Код обязателен") + await client.disconnect() + return try: await client.sign_in(phone, code) except Exception as e: - if "PASSWORD" in str(e): + error_msg = str(e) + + if "SESSION_PASSWORD_NEEDED" in error_msg or "2FA" in error_msg.upper(): # Запрос 2FA пароля - password = input("🔒 Введите 2FA пароль: ").strip() - await client.sign_in(password=password) + logger.info("") + logger.info("🔒 Требуется 2FA пароль") + password = input("Введите пароль: ").strip() + + try: + await client.sign_in(password=password) + except Exception as e2: + logger.error(f"❌ Ошибка 2FA: {e2}") + await client.disconnect() + return + elif "PHONE_CODE_INVALID" in error_msg: + logger.error("❌ Неверный код") + await client.disconnect() + return else: - logger.error(f"Ошибка входа: {e}") + logger.error(f"❌ Ошибка входа: {e}") + await client.disconnect() return # Получаем информацию о пользователе me = await client.get_me() - logger.info(f"✅ Успешная авторизация: {me.first_name} @{me.username or 'no_username'}") + logger.info("") + logger.info("✅ Успешная авторизация!") + logger.info(f"👤 {me.first_name} {me.last_name or ''}") + logger.info(f"📱 @{me.username or 'нет username'}") + logger.info(f"🆔 ID: {me.id}") + logger.info(f"📞 {me.phone}") + logger.info("") # Сохраняем сессию session_string = client.session.save() @@ -110,19 +143,24 @@ async def main(): with open(session_path, 'w', encoding='utf-8') as f: f.write(session_string) - logger.info(f"💾 Сессия сохранена: {session_path}") + logger.info("💾 Сессия сохранена:") + logger.info(f"📁 {session_path}") logger.info("") - logger.info("Следующие шаги:") - logger.info("1. Скопируйте файл сессии в папку sessions/") - logger.info("2. Запустите бота: python bot/controller.py") - logger.info("3. Запустите воркера: python bot/worker.py") + logger.info("📋 Следующие шаги:") + logger.info("1. Файл сессии уже в папке sessions/") + logger.info("2. Перезапустите worker: docker-compose restart worker") + logger.info("3. Проверьте логи: docker-compose logs -f worker") + logger.info("") + logger.info("=" * 50) await client.disconnect() except KeyboardInterrupt: - logger.info("\n❌ Отменено пользователем") + logger.info("") + logger.info("❌ Отменено пользователем") except Exception as e: logger.error(f"❌ Ошибка: {e}") + logger.exception("Полный стек:") raise diff --git a/bot/controller.py b/bot/controller.py index 61a6790..ed4dd43 100644 --- a/bot/controller.py +++ b/bot/controller.py @@ -12,7 +12,8 @@ from bot.db import ( 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 + remove_target_group, toggle_target_group, is_target_group, + get_pending_comments_by_session, get_pending_comments_by_group ) from bot.ollama import generate_comment from bot.keyboard import ( @@ -381,15 +382,27 @@ class CommentBot: if data == "stats": await self._callback_stats(callback) elif data == "sessions": - await self._callback_sessions(callback) + await self._callback_sessions_list(callback) + elif data.startswith("session_select:"): + session_file = data.split(":")[1] + await self._callback_session_pending(callback, session_file) elif data == "pending": - await self._callback_pending(callback) + await self._callback_pending_groups(callback) + elif data.startswith("group_pending:"): + group_id = data.split(":")[1] + await self._callback_group_pending(callback, group_id) elif data == "groups": await self._callback_groups(callback) elif data == "group_add": await self._callback_group_add(callback) - elif data.startswith("group_"): - await self._callback_group_action(callback) + elif data.startswith("group_info:"): + await self._callback_group_info(callback, data) + elif data.startswith("group_pause:"): + await self._callback_group_action(callback, "pause", data) + elif data.startswith("group_resume:"): + await self._callback_group_action(callback, "resume", data) + elif data.startswith("group_delete:"): + await self._callback_group_action(callback, "delete", data) elif data.startswith("approve:"): await self._callback_approve(callback) elif data.startswith("reject:"): @@ -400,8 +413,16 @@ class CommentBot: 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) + elif data.startswith("session_status:"): + await self._callback_session_action(callback, "status", data) + elif data.startswith("session_pause:"): + await self._callback_session_action(callback, "pause", data) + elif data.startswith("session_resume:"): + await self._callback_session_action(callback, "resume", data) + elif data.startswith("session_delete:"): + await self._callback_session_action(callback, "delete", data) + elif data == "main_menu": + await self._callback_main_menu(callback) else: await callback.answer("Неизвестная команда") @@ -462,6 +483,99 @@ class CommentBot: await callback.answer() + + async def _callback_sessions_list(self, callback: CallbackQuery): + """Callback для списка сессий""" + sessions = get_all_sessions() + + if not sessions: + text = "❌ Нет сессий\n\nДобавьте файлы сессий в `sessions/`" + await callback.message.edit_text(text, reply_markup=create_back_keyboard()) + else: + text = "👥 **Сессии**\n\nВыберите сессию для просмотра комментариев:\n\n" + for session in sessions: + status = "🟢" if session['is_active'] else "🔴" + username = f"@{session['username']}" if session.get('username') else "" + text += f"{status} `{session['session_file']}` {username}\n" + + await callback.message.edit_text( + text, + parse_mode="Markdown", + reply_markup=create_sessions_list_keyboard(sessions) + ) + + await callback.answer() + + async def _callback_session_pending(self, callback: CallbackQuery, session_file: str): + """Callback для просмотра pending комментариев сессии""" + pending = get_pending_comments_by_session(session_file) + + if not pending: + await callback.message.edit_text( + f"✅ Нет ожидающих комментариев для `{session_file}`", + reply_markup=create_back_keyboard() + ) + else: + await callback.message.edit_text( + f"📝 Ожидают модерации ({len(pending)}):\n\nСессия: `{session_file}`", + reply_markup=create_back_keyboard() + ) + for comment in pending[:10]: + await self._send_moderation_message( + chat_id=callback.message.chat.id, + comment=comment + ) + + await callback.answer() + + async def _callback_pending_groups(self, callback: CallbackQuery): + """Callback для выбора группы из pending""" + groups = get_all_target_groups() + + if not groups: + text = "📋 **Нет групп**\n\nДобавьте группу через /add_group" + await callback.message.edit_text(text, parse_mode="Markdown", reply_markup=create_back_keyboard()) + else: + text = "📋 **Группы**\n\nВыберите группу для просмотра комментариев:\n\n" + for group in groups: + name = group['group_name'] or f"Группа {group['group_id']}" + text += f"📢 `{name}`\n" + + await callback.message.edit_text( + text, + parse_mode="Markdown", + reply_markup=create_groups_list_for_pending_keyboard(groups) + ) + + await callback.answer() + + async def _callback_group_pending(self, callback: CallbackQuery, group_id: str): + """Callback для просмотра pending комментариев группы""" + pending = get_pending_comments_by_group(group_id) + + if not pending: + await callback.message.edit_text( + f"✅ Нет ожидающих комментариев для этой группы", + reply_markup=create_back_keyboard() + ) + else: + await callback.message.edit_text( + f"📝 Ожидают модерации ({len(pending)}):\n\nГруппа: `{group_id}`", + reply_markup=create_back_keyboard() + ) + for comment in pending[:10]: + await self._send_moderation_message( + chat_id=callback.message.chat.id, + comment=comment + ) + + await callback.answer() + + async def _callback_main_menu(self, callback: CallbackQuery): + """Callback для возврата в главное меню""" + await self.cmd_start(callback.message) + await callback.answer() + async def _callback_settings(self, callback: CallbackQuery): """Callback для настроек""" text = ( @@ -622,16 +736,38 @@ class CommentBot: channel_id = comment.get('channel_id') or comment['chat_id'] post_url = f"https://t.me/c/{channel_id}/{comment['message_id']}" + # Получаем название группы/канала из БД + # Ищем по chat_id (группа комментариев) или по channel_id (канал) + groups = get_all_target_groups() + group = None + + # Сначала ищем по chat_id (группа комментариев) + for g in groups: + if str(g['group_id']) == str(comment['chat_id']): + group = g + break + # Или по channel_id (канал) + if str(g.get('group_id')) == str(channel_id): + group = g + break + # Или по comments_group_id + if str(g.get('comments_group_id')) == str(comment['chat_id']): + group = g + break + + group_name = group['group_name'] if group and group.get('group_name') else f"Канал {channel_id}" + session_info = "" if comment.get('session_file'): session_info = f"👤 Сессия: `{comment['session_file']}`\n\n" - + post_text = comment.get('post_text', 'Текст поста не сохранён') if len(post_text) > 300: post_text = post_text[:300] + "..." - + text = ( - f"📝 **Новый комментарий**\n\n" + f"📝 Новый комментарий\n\n" + f"📢 Канал: **{group_name}**\n" f"🔗 Пост: {post_url}\n" f"{session_info}" f"📄 Текст поста:\n" @@ -640,13 +776,13 @@ class CommentBot: 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, diff --git a/bot/db.py b/bot/db.py index 8e919a9..625a20f 100644 --- a/bot/db.py +++ b/bot/db.py @@ -264,6 +264,48 @@ def get_pending_comments() -> list: conn.close() +def get_pending_comments_by_session(session_file: str) -> list: + """Получение ожидающих комментариев для конкретной сессии""" + try: + conn = get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT * FROM comments + WHERE status = 'pending' AND session_file = ? + ORDER BY created_at DESC + ''', (session_file,)) + + 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_pending_comments_by_group(group_id: str) -> list: + """Получение ожидающих комментариев для конкретной группы""" + try: + conn = get_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT * FROM comments + WHERE status = 'pending' AND chat_id = ? + ORDER BY created_at DESC + ''', (group_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 save_session(session_file: str, user_info: dict) -> bool: diff --git a/bot/keyboard.py b/bot/keyboard.py index 1a6c7a6..2a29832 100644 --- a/bot/keyboard.py +++ b/bot/keyboard.py @@ -111,10 +111,37 @@ def create_sessions_list_keyboard(sessions: list) -> InlineKeyboardMarkup: for session in sessions: session_file = session['session_file'] status = "🟢" if session['is_active'] else "🔴" + username = f"@{session['username']}" if session.get('username') else "" builder.button( - text=f"{status} {session_file}", - callback_data=f"session_info:{session_file}" + text=f"{status} {session_file} {username}", + callback_data=f"session_select:{session_file}" ) + builder.adjust(1) + builder.button(text="🔙 Назад", callback_data="main_menu") + return builder.as_markup() + + +def create_groups_list_for_pending_keyboard(groups: list) -> InlineKeyboardMarkup: + """Клавиатура со списком групп для выбора pending комментариев""" + builder = InlineKeyboardBuilder() + + for group in groups: + group_id = group['group_id'] + name = group['group_name'] or f"Группа {group_id}" + builder.button( + text=f"📢 {name}", + callback_data=f"group_pending:{group_id}" + ) + + builder.adjust(1) + builder.button(text="🔙 Назад", callback_data="main_menu") + return builder.as_markup() + + +def create_back_keyboard() -> InlineKeyboardMarkup: + """Клавиатура с кнопкой Назад""" + builder = InlineKeyboardBuilder() + builder.button(text="🔙 Назад", callback_data="main_menu") builder.adjust(1) return builder.as_markup() diff --git a/bot/worker.py b/bot/worker.py index af2dcd0..441fe3b 100644 --- a/bot/worker.py +++ b/bot/worker.py @@ -26,9 +26,10 @@ logger = logging.getLogger('worker') class CommentWorker: """Воркер для отправки комментариев от имени пользователей""" - + def __init__(self): self.clients: dict[str, TelegramClient] = {} + self.log_client: TelegramClient = None # Клиент для отправки логов self.running = False async def start(self): @@ -46,14 +47,37 @@ class CommentWorker: logger.warning("Нет активных сессий. Добавьте файлы в sessions/") return + # Создаём клиента для отправки логов (используем первую сессию) + if self.clients: + first_session = list(self.clients.keys())[0] + self.log_client = self.clients[first_session] + logger.info(f"Лог-клиент: {first_session}") + self.running = True + # Отправляем уведомление о запуске + await self.send_log_message("🚀 Worker запущен") + # Запускаем задачи параллельно await asyncio.gather( self.listen_for_new_posts(), self.monitor_approved_comments() ) + async def send_log_message(self, message: str): + """Отправка сообщения в лог-группу""" + if not self.log_client or not LOG_GROUP_ID: + return + + try: + await self.log_client.send_message( + int(LOG_GROUP_ID), + message, + parse_mode='HTML' + ) + except Exception as e: + logger.debug(f"Не удалось отправить лог: {e}") + async def stop(self): """Остановка воркера""" logger.info("Остановка Comment Worker...") @@ -164,45 +188,59 @@ class CommentWorker: while self.running: current_time = datetime.now().timestamp() + + # Проверяем новые группы каждые 10 секунд 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}") + 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(): + # Проверяем что клиент подключён + if not client.is_connected(): + logger.warning(f"Клиент {session_file} не подключён") + continue + + # Регистрируем обработчик @client.on(events.NewMessage(chats=group_id)) async def handle_new_post(event): message = event.message - logger.info(f"Новый пост в группе {group_id}: {message.id}") + logger.info(f"📬 Новый пост в группе {group_id}: {message.id}") + + # Проверяем что это не бот + if message.from_id and message.from_id.user_id == (await client.get_me()).id: + return + + # Отправляем уведомление в лог-группу + post_preview = message.text[:100] + "..." if len(message.text or "") > 100 else (message.text or "Без текста") + log_message = ( + f"📬 Новый пост\n\n" + f"📢 Группа: {group_id}\n" + f"🔗 Пост: {message.id}\n" + f"📄 Текст: {post_preview}" + ) + await self.send_log_message(log_message) + 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} удалена, сброс кэша") + logger.info(f"✅ Обработчик зарегистрирован для {group_id}") # Удаляем обработчики для удалённых групп for group_id in list(group_handlers.keys()): if group_id not in new_group_ids: + logger.info(f"❌ Группа {group_id} удалена из мониторинга") del group_handlers[group_id] scanned_groups.discard(group_id) - logger.info(f"Группа {group_id} удалена из мониторинга") last_groups_check = current_time @@ -332,8 +370,27 @@ class CommentWorker: 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})") + # Вступаем в группу комментариев ПЕРЕД тем как получать сущность + try: + await session_manager.join_channel(session_file, comments_group_id) + logger.info(f"✅ Вступил в группу комментариев: {comments_group_id}") + except Exception as e: + logger.debug(f"Уже в группе: {e}") + + # Теперь получаем сущность группы комментариев + comments_group = None + for try_id in [comments_group_id, abs(int(comments_group_id)), -abs(int(comments_group_id))]: + try: + comments_group = await client.get_entity(PeerChannel(abs(int(try_id)))) + logger.info(f"Группа комментариев: {comments_group.title} (ID: {try_id})") + break + except Exception as e: + logger.debug(f"Не удалось получить сущность {try_id}: {e}") + continue + + if not comments_group: + logger.error(f"Не удалось найти группу комментариев {comments_group_id}") + return from telethon.tl.functions.channels import GetFullChannelRequest channel_full = await client(GetFullChannelRequest(comments_group)) @@ -346,25 +403,24 @@ class CommentWorker: 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 - ) + target_message_in_channel = None + for try_id in [linked_chat_id, abs(int(linked_chat_id)), -abs(int(linked_chat_id))]: + try: + target_message_in_channel = await client.get_messages( + PeerChannel(abs(int(try_id))), + ids=message_id_in_channel + ) + if target_message_in_channel: + logger.info(f"Найдено сообщение в канале: {target_message_in_channel.id}") + break + except Exception as e: + logger.debug(f"Не удалось получить сообщение {try_id}/{message_id_in_channel}: {e}") + continue 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)