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
This commit is contained in:
2026-02-28 01:44:40 +03:00
parent a18ad30961
commit bb27161524
8 changed files with 537 additions and 99 deletions

5
.gitignore vendored
View File

@@ -55,6 +55,8 @@ logs/
*.swp *.swp
*.swo *.swo
*~ *~
# macOS
.DS_Store .DS_Store
# Testing # Testing
@@ -69,3 +71,6 @@ docs/_build/
tmp/ tmp/
temp/ temp/
*.tmp *.tmp
# Migration scripts (optional)
migrate_db.py

110
DEPLOYMENT_CHECKLIST.md Normal file
View File

@@ -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
---
**Готово к отправке!** 🎉

View File

@@ -1,6 +1,6 @@
# Batch Bot - Telegram Comment Bot # Batch Bot - Telegram Comment Bot
Автоматический бот для генерации и публикации комментариев в Telegram от имени нескольких пользователей. Автоматический бот для генерации и публикации комментариев в Telegram от имени нескольких пользователей с использованием локальной LLM (Ollama).
## 🏗 Архитектура ## 🏗 Архитектура
@@ -30,8 +30,9 @@
-**Модерация** — inline-кнопки для одобрения/отклонения -**Модерация** — inline-кнопки для одобрения/отклонения
-**Редактирование** — возможность изменить текст перед отправкой -**Редактирование** — возможность изменить текст перед отправкой
-**Статистика** — учёт сгенерированных/отправленных комментариев -**Статистика** — учёт сгенерированных/отправленных комментариев
-**Безопасность** — разделение контроллера и воркеров -**Фильтрация** — просмотр комментариев по сессиям и группам
-**Docker** — полная контейнеризация -**Docker** — полная контейнеризация
-**Уведомления** — уведомления о новых постах в лог-группу
## 🚀 Быстрый старт ## 🚀 Быстрый старт
@@ -66,13 +67,19 @@ pip install -r requirements.txt
python auth.py python auth.py
``` ```
Введите номер телефона и код из Telegram. Введите API credentials, номер телефона и код из Telegram.
**Для нескольких аккаунтов:** **Для нескольких аккаунтов:**
- Запустите `python auth.py` несколько раз - Запустите `python auth.py` несколько раз
- Или скопируйте `.session` файлы в `sessions/` - Или скопируйте `.session` файлы в `sessions/`
### 3. Запуск Docker ### 3. Миграция БД (если обновляетесь)
```bash
python migrate_db.py
```
### 4. Запуск Docker
```bash ```bash
# Сборка и запуск # Сборка и запуск
@@ -98,12 +105,24 @@ docker-compose down
|---------|----------| |---------|----------|
| `/start` | Главное меню | | `/start` | Главное меню |
| `/stats` | Статистика | | `/stats` | Статистика |
| `/pending` | Ожидающие комментарии | | `/pending` | Ожидающие комментарии (по группам) |
| `/sessions` | Сессии | | `/sessions` | Сессии (по сессиям) |
| `/groups` | Управление группами | | `/groups` | Управление группами |
| `/add_group ID` | Добавить группу | | `/add_group ID` | Добавить группу |
| `/help` | Справка | | `/help` | Справка |
### Фильтрация комментариев
**По сессиям:**
1. Нажмите "👥 Сессии"
2. Выберите сессию
3. Просмотрите комментарии этой сессии
**По группам:**
1. Нажмите "📝 Ожидающие"
2. Выберите группу
3. Просмотрите комментарии этой группы
### Добавление группы ### Добавление группы
**Через команду:** **Через команду:**
@@ -158,6 +177,7 @@ batch-bot/
├── Dockerfile # Образ для controller/worker ├── Dockerfile # Образ для controller/worker
├── requirements.txt # Python зависимости ├── requirements.txt # Python зависимости
├── auth.py # Скрипт авторизации ├── auth.py # Скрипт авторизации
├── migrate_db.py # Скрипт миграции БД
├── prompt.txt # Шаблон для LLM ├── prompt.txt # Шаблон для LLM
├── bot/ ├── bot/
│ ├── config.py # Конфигурация │ ├── config.py # Конфигурация
@@ -257,6 +277,10 @@ OLLAMA_URL=http://host.docker.internal:11434
- Проверьте что аккаунт вступил в группу комментариев - Проверьте что аккаунт вступил в группу комментариев
- Worker автоматически вступает при отправке - Worker автоматически вступает при отправке
**"Could not find the input entity":**
- Аккаунт должен быть участником группы
- Worker автоматически вступает при сканировании
## 📝 Лицензия ## 📝 Лицензия
MIT MIT

130
auth.py
View File

@@ -1,14 +1,13 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Скрипт для создания сессии Telegram Telegram Session Creator
Запустите этот скрипт для авторизации и создания файла сессии Создаёт сессию для бота через интерактивную авторизацию
""" """
import asyncio import asyncio
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv
from loguru import logger from loguru import logger
from telethon import TelegramClient from telethon import TelegramClient
from telethon.sessions import StringSession from telethon.sessions import StringSession
@@ -27,80 +26,114 @@ logger.add(
level="DEBUG" level="DEBUG"
) )
# Загрузка переменных окружения # Пути
load_dotenv() BASE_DIR = Path(__file__).parent
SESSIONS_DIR = BASE_DIR / "sessions"
# Конфигурация SESSIONS_DIR.mkdir(exist_ok=True)
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")
async def main(): async def main():
"""Создание сессии""" """Создание сессии"""
try: try:
# Проверка конфигурации logger.info("=" * 50)
if not all([API_ID, API_HASH]): logger.info("🔐 Telegram Session Creator")
logger.error("❌ TELEGRAM_API_ID и TELEGRAM_API_HASH должны быть заданы в .env") 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 return
# Создаём директорию для сессий logger.info("")
SESSIONS_DIR.mkdir(exist_ok=True) logger.info("📱 Введите номер телефона")
logger.info("Формат: +79991234567 (с кодом страны)")
logger.info("")
logger.info("🔐 Telegram Session Creator") phone = input("Номер телефона: ").strip()
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 = PHONE
if not phone: if not phone:
phone = input("📱 Введите номер телефона (с +7): ").strip() logger.error("❌ Номер телефона обязателен")
return
# Создаём клиента # Создаём клиента
client = TelegramClient( client = TelegramClient(
StringSession(), StringSession(),
API_ID, int(api_id),
API_HASH, api_hash,
device_model="comment_bot", device_model="Desktop (X64)",
system_version="Linux", system_version="Windows 11",
app_version="1.0", app_version="3.2.2",
lang_code="ru" lang_code="en",
system_lang_code="en-US"
) )
logger.info("Подключение к Telegram...") logger.info("")
logger.info("⏳ Подключение к Telegram...")
await client.connect() await client.connect()
if not await client.is_user_authorized(): if not await client.is_user_authorized():
logger.info("Отправка кода подтверждения...") logger.info("📲 Отправка кода подтверждения...")
try: try:
await client.send_code_request(phone) await client.send_code_request(phone)
logger.info(f"✅ Код отправлен на {phone}")
except Exception as e: except Exception as e:
logger.error(f"Ошибка отправки кода: {e}") logger.error(f"Ошибка отправки кода: {e}")
await client.disconnect()
return return
# Ввод кода # Ввод кода
code = input("📲 Введите код из Telegram: ").strip() logger.info("")
code = input("Введите код из Telegram: ").strip()
if not code:
logger.error("❌ Код обязателен")
await client.disconnect()
return
try: try:
await client.sign_in(phone, code) await client.sign_in(phone, code)
except Exception as e: 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 пароля # Запрос 2FA пароля
password = input("🔒 Введите 2FA пароль: ").strip() logger.info("")
logger.info("🔒 Требуется 2FA пароль")
password = input("Введите пароль: ").strip()
try:
await client.sign_in(password=password) 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: else:
logger.error(f"Ошибка входа: {e}") logger.error(f"Ошибка входа: {e}")
await client.disconnect()
return return
# Получаем информацию о пользователе # Получаем информацию о пользователе
me = await client.get_me() 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() session_string = client.session.save()
@@ -110,19 +143,24 @@ async def main():
with open(session_path, 'w', encoding='utf-8') as f: with open(session_path, 'w', encoding='utf-8') as f:
f.write(session_string) f.write(session_string)
logger.info(f"💾 Сессия сохранена: {session_path}") logger.info("💾 Сессия сохранена:")
logger.info(f"📁 {session_path}")
logger.info("") logger.info("")
logger.info("Следующие шаги:") logger.info("📋 Следующие шаги:")
logger.info("1. Скопируйте файл сессии в папку sessions/") logger.info("1. Файл сессии уже в папке sessions/")
logger.info("2. Запустите бота: python bot/controller.py") logger.info("2. Перезапустите worker: docker-compose restart worker")
logger.info("3. Запустите воркера: python bot/worker.py") logger.info("3. Проверьте логи: docker-compose logs -f worker")
logger.info("")
logger.info("=" * 50)
await client.disconnect() await client.disconnect()
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("\n❌ Отменено пользователем") logger.info("")
logger.info("❌ Отменено пользователем")
except Exception as e: except Exception as e:
logger.error(f"❌ Ошибка: {e}") logger.error(f"❌ Ошибка: {e}")
logger.exception("Полный стек:")
raise raise

View File

@@ -12,7 +12,8 @@ from bot.db import (
increment_regeneration, get_all_sessions, get_active_sessions, increment_regeneration, get_all_sessions, get_active_sessions,
toggle_session, delete_session, get_summary_stats, get_stats, toggle_session, delete_session, get_summary_stats, get_stats,
update_stats, add_target_group, get_target_groups, get_all_target_groups, 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.ollama import generate_comment
from bot.keyboard import ( from bot.keyboard import (
@@ -381,15 +382,27 @@ class CommentBot:
if data == "stats": if data == "stats":
await self._callback_stats(callback) await self._callback_stats(callback)
elif data == "sessions": 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": 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": elif data == "groups":
await self._callback_groups(callback) await self._callback_groups(callback)
elif data == "group_add": elif data == "group_add":
await self._callback_group_add(callback) await self._callback_group_add(callback)
elif data.startswith("group_"): elif data.startswith("group_info:"):
await self._callback_group_action(callback) 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:"): elif data.startswith("approve:"):
await self._callback_approve(callback) await self._callback_approve(callback)
elif data.startswith("reject:"): elif data.startswith("reject:"):
@@ -400,8 +413,16 @@ class CommentBot:
await self._callback_edit(callback) await self._callback_edit(callback)
elif data.startswith("edit_cancel:"): elif data.startswith("edit_cancel:"):
await self._callback_edit_cancel(callback) await self._callback_edit_cancel(callback)
elif data.startswith("session_"): elif data.startswith("session_status:"):
await self._callback_session_action(callback) 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: else:
await callback.answer("Неизвестная команда") await callback.answer("Неизвестная команда")
@@ -462,6 +483,99 @@ class CommentBot:
await callback.answer() 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): async def _callback_settings(self, callback: CallbackQuery):
"""Callback для настроек""" """Callback для настроек"""
text = ( text = (
@@ -622,6 +736,27 @@ class CommentBot:
channel_id = comment.get('channel_id') or comment['chat_id'] channel_id = comment.get('channel_id') or comment['chat_id']
post_url = f"https://t.me/c/{channel_id}/{comment['message_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 = "" session_info = ""
if comment.get('session_file'): if comment.get('session_file'):
session_info = f"👤 Сессия: `{comment['session_file']}`\n\n" session_info = f"👤 Сессия: `{comment['session_file']}`\n\n"
@@ -631,7 +766,8 @@ class CommentBot:
post_text = post_text[:300] + "..." post_text = post_text[:300] + "..."
text = ( text = (
f"📝 **Новый комментарий**\n\n" f"📝 Новый комментарий\n\n"
f"📢 Канал: **{group_name}**\n"
f"🔗 Пост: {post_url}\n" f"🔗 Пост: {post_url}\n"
f"{session_info}" f"{session_info}"
f"📄 Текст поста:\n" f"📄 Текст поста:\n"

View File

@@ -264,6 +264,48 @@ def get_pending_comments() -> list:
conn.close() 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: def save_session(session_file: str, user_info: dict) -> bool:

View File

@@ -111,10 +111,37 @@ def create_sessions_list_keyboard(sessions: list) -> InlineKeyboardMarkup:
for session in sessions: for session in sessions:
session_file = session['session_file'] session_file = session['session_file']
status = "🟢" if session['is_active'] else "🔴" status = "🟢" if session['is_active'] else "🔴"
username = f"@{session['username']}" if session.get('username') else ""
builder.button( builder.button(
text=f"{status} {session_file}", text=f"{status} {session_file} {username}",
callback_data=f"session_info:{session_file}" 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) builder.adjust(1)
return builder.as_markup() return builder.as_markup()

View File

@@ -29,6 +29,7 @@ class CommentWorker:
def __init__(self): def __init__(self):
self.clients: dict[str, TelegramClient] = {} self.clients: dict[str, TelegramClient] = {}
self.log_client: TelegramClient = None # Клиент для отправки логов
self.running = False self.running = False
async def start(self): async def start(self):
@@ -46,14 +47,37 @@ class CommentWorker:
logger.warning("Нет активных сессий. Добавьте файлы в sessions/") logger.warning("Нет активных сессий. Добавьте файлы в sessions/")
return 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 self.running = True
# Отправляем уведомление о запуске
await self.send_log_message("🚀 Worker запущен")
# Запускаем задачи параллельно # Запускаем задачи параллельно
await asyncio.gather( await asyncio.gather(
self.listen_for_new_posts(), self.listen_for_new_posts(),
self.monitor_approved_comments() 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): async def stop(self):
"""Остановка воркера""" """Остановка воркера"""
logger.info("Остановка Comment Worker...") logger.info("Остановка Comment Worker...")
@@ -164,45 +188,59 @@ class CommentWorker:
while self.running: while self.running:
current_time = datetime.now().timestamp() current_time = datetime.now().timestamp()
# Проверяем новые группы каждые 10 секунд
if current_time - last_groups_check > 10: if current_time - last_groups_check > 10:
target_groups = get_target_groups() target_groups = get_target_groups()
new_group_ids = [str(g['group_id']) for g in target_groups] new_group_ids = [str(g['group_id']) for g in target_groups]
# Проверяем, есть ли группы которые были удалены и добавлены заново # Добавляем обработчики для новых групп
for group_id in new_group_ids: for group_id in new_group_ids:
if group_id not in group_handlers: if group_id not in group_handlers:
logger.info(f"Добавлена группа для мониторинга: {group_id}") logger.info(f" Добавлена группа для мониторинга: {group_id}")
# Сканируем последние 10 сообщений новой группы # Сканируем последние 10 сообщений новой группы
# Всегда сканируем при добавлении, даже если уже было
await self.scan_group(group_id, limit=10) await self.scan_group(group_id, limit=10)
scanned_groups.add(group_id) scanned_groups.add(group_id)
# Регистрируем обработчик для каждой сессии
for session_file, client in self.clients.items(): 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)) @client.on(events.NewMessage(chats=group_id))
async def handle_new_post(event): async def handle_new_post(event):
message = event.message 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"📬 <b>Новый пост</b>\n\n"
f"📢 Группа: <code>{group_id}</code>\n"
f"🔗 Пост: <a href='https://t.me/c/{str(group_id).lstrip('-')}/{message.id}'>{message.id}</a>\n"
f"📄 Текст: <i>{post_preview}</i>"
)
await self.send_log_message(log_message)
await self.process_message(message, session_file, client, group_id) await self.process_message(message, session_file, client, group_id)
group_handlers[group_id] = True group_handlers[group_id] = True
else: logger.info(f"✅ Обработчик зарегистрирован для {group_id}")
# Группа уже обрабатывается, но проверяем не была ли она удалена и добавлена снова
# Если была - очищаем 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()): for group_id in list(group_handlers.keys()):
if group_id not in new_group_ids: if group_id not in new_group_ids:
logger.info(f"❌ Группа {group_id} удалена из мониторинга")
del group_handlers[group_id] del group_handlers[group_id]
scanned_groups.discard(group_id) scanned_groups.discard(group_id)
logger.info(f"Группа {group_id} удалена из мониторинга")
last_groups_check = current_time 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}") 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 from telethon.tl.functions.channels import GetFullChannelRequest
channel_full = await client(GetFullChannelRequest(comments_group)) channel_full = await client(GetFullChannelRequest(comments_group))
@@ -346,25 +403,24 @@ class CommentWorker:
logger.info(f"Linked chat ID: {linked_chat_id}") logger.info(f"Linked chat ID: {linked_chat_id}")
# Получаем сообщение в КАНАЛЕ # Получаем сообщение в КАНАЛЕ
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( target_message_in_channel = await client.get_messages(
PeerChannel(abs(int(linked_chat_id))), PeerChannel(abs(int(try_id))),
ids=message_id_in_channel 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: if not target_message_in_channel:
logger.error(f"Сообщение {message_id_in_channel} не найдено в канале {linked_chat_id}") logger.error(f"Сообщение {message_id_in_channel} не найдено в канале {linked_chat_id}")
return 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= (как в старом проекте) # ОТПРАВЛЯЕМ В КАНАЛ с comment_to= (как в старом проекте)
# Telegram автоматически направит комментарий в группу комментариев # Telegram автоматически направит комментарий в группу комментариев
delay = random.uniform(COMMENT_DELAY_MIN, COMMENT_DELAY_MAX) delay = random.uniform(COMMENT_DELAY_MIN, COMMENT_DELAY_MAX)