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)