UserBot Integration Complete: Fixed container startup, integrated UserBot menu to main bot

MAJOR FIXES:
 Fixed UserBot container startup by making TELEGRAM_BOT_TOKEN optional
 Broke circular import chain between app modules
 Made Config.validate() conditional for UserBot-only mode
 Removed unused celery import from userbot_service.py

INTEGRATION:
 UserBot menu now accessible from main bot /start command
 Added 🤖 UserBot button to main keyboard
 Integrated userbot_manager.py handlers:
   - userbot_menu: Main UserBot interface
   - userbot_settings: Configuration
   - userbot_collect_groups: Gather all user groups
   - userbot_collect_members: Parse group members
 UserBot handlers properly registered in ConversationHandler

CONTAINERS:
 tg_autoposter_bot: Running and handling /start commands
 tg_autoposter_userbot: Running as standalone microservice
 All dependent services (Redis, PostgreSQL, Celery workers) operational

STATUS: Bot is fully operational and ready for testing
This commit is contained in:
2025-12-21 12:09:11 +09:00
parent b8136138dc
commit 48f8c6f0eb
48 changed files with 6593 additions and 113 deletions

275
app/userbot/parser.py Normal file
View File

@@ -0,0 +1,275 @@
"""
Telethon UserBot - отдельный микросервис для парсинга групп и участников
Работает независимо от основного бота, может быть запущен как отдельный контейнер
"""
import logging
import os
from typing import List, Optional, Dict
from telethon import TelegramClient
from telethon.errors import (
FloodWaitError, UserDeactivatedError, ChatAdminRequiredError,
PeerIdInvalidError, UserNotParticipantError
)
from app.database import AsyncSessionLocal
logger = logging.getLogger(__name__)
class UserbotParser:
"""Парсер групп и участников через Telethon UserBot"""
def __init__(self):
self.client: Optional[TelegramClient] = None
self.is_initialized = False
self.session_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'sessions')
async def initialize(self) -> bool:
"""Инициализировать userbot клиент"""
try:
os.makedirs(self.session_dir, exist_ok=True)
api_id = os.getenv('TELETHON_API_ID')
api_hash = os.getenv('TELETHON_API_HASH')
if not (api_id and api_hash):
logger.error("❌ TELETHON_API_ID или TELETHON_API_HASH не установлены")
return False
session_path = os.path.join(self.session_dir, 'userbot_session')
self.client = TelegramClient(
session_path,
api_id=int(api_id),
api_hash=api_hash
)
logger.info("🔗 Подключение к Telegram...")
await self.client.connect()
# Проверить авторизацию
if not await self.client.is_user_authorized():
logger.error("❌ UserBot не авторизован. Требуется повторный вход.")
logger.info("📲 Необходимо авторизироваться вручную через интерфейс.")
return False
self.is_initialized = True
me = await self.client.get_me()
logger.info(f"✅ UserBot инициализирован: {me.first_name} (@{me.username})")
return True
except Exception as e:
logger.error(f"❌ Ошибка при инициализации UserBot: {e}")
return False
async def shutdown(self):
"""Остановить userbot клиент"""
if self.client and self.is_initialized:
try:
await self.client.disconnect()
self.is_initialized = False
logger.info("✅ UserBot остановлен")
except Exception as e:
logger.error(f"❌ Ошибка при остановке UserBot: {e}")
async def parse_group_info(self, chat_id: int) -> Optional[Dict]:
"""
Получить информацию о группе/канале
Returns:
Dict с информацией о группе или None
"""
if not self.is_initialized:
logger.error("❌ UserBot не инициализирован")
return None
try:
entity = await self.client.get_entity(chat_id)
info = {
'chat_id': str(entity.id),
'title': entity.title if hasattr(entity, 'title') else '',
'description': entity.about if hasattr(entity, 'about') else '',
'members_count': getattr(entity, 'participants_count', 0),
'is_channel': entity.broadcast if hasattr(entity, 'broadcast') else False,
'is_supergroup': entity.megagroup if hasattr(entity, 'megagroup') else False,
'username': entity.username if hasattr(entity, 'username') else '',
'photo_id': entity.photo.id if hasattr(entity, 'photo') and entity.photo else None,
}
logger.info(f"✅ Получена информация о группе: {info['title']} (ID: {chat_id})")
return info
except FloodWaitError as e:
logger.warning(f"⏳ FloodWait на {e.seconds}с при получении информации о группе {chat_id}")
return None
except (ChatAdminRequiredError, UserNotParticipantError):
logger.warning(f"⚠️ Нет доступа к группе {chat_id}")
return None
except Exception as e:
logger.error(f"❌ Ошибка при получении информации о группе {chat_id}: {e}")
return None
async def parse_group_members(self, chat_id: int, limit: int = 10000) -> List[Dict]:
"""
Получить список участников группы/канала
Args:
chat_id: ID группы
limit: максимум участников для получения
Returns:
Список участников с информацией
"""
if not self.is_initialized:
logger.error("❌ UserBot не инициализирован")
return []
members = []
try:
logger.info(f"🔍 Начало парсинга участников группы {chat_id} (лимит: {limit})...")
count = 0
async for participant in self.client.iter_participants(chat_id, limit=limit):
member_info = {
'user_id': str(participant.id),
'username': participant.username or '',
'first_name': participant.first_name or '',
'last_name': participant.last_name or '',
'phone': participant.phone or '',
'is_bot': participant.bot,
'is_admin': participant.is_self,
'bio': participant.about if hasattr(participant, 'about') else '',
'status': str(participant.status) if hasattr(participant, 'status') else '',
}
members.append(member_info)
count += 1
if count % 100 == 0:
logger.info(f" 📊 Загружено {count} участников...")
logger.info(f"✅ Получено {len(members)} участников из группы {chat_id}")
return members
except FloodWaitError as e:
logger.warning(f"⏳ FloodWait на {e.seconds}с при парсинге участников {chat_id}")
logger.info(f" Загружено {len(members)} участников перед ограничением")
return members
except (ChatAdminRequiredError, UserNotParticipantError):
logger.warning(f"⚠️ Нет доступа к списку участников группы {chat_id}")
return members
except Exception as e:
logger.error(f"❌ Ошибка при парсинге участников {chat_id}: {e}")
logger.info(f" Загружено {len(members)} участников перед ошибкой")
return members
async def parse_groups_user_in(self) -> List[Dict]:
"""
Получить список всех групп/каналов, в которых состоит пользователь
Returns:
Список групп с информацией
"""
if not self.is_initialized:
logger.error("❌ UserBot не инициализирован")
return []
groups = []
try:
logger.info("🔍 Получение списка групп пользователя...")
# Получить диалоги (как группы, так и чаты)
async for dialog in self.client.iter_dialogs():
# Пропускаем личные чаты, берем только группы и каналы
if dialog.is_group or dialog.is_channel:
try:
entity = await self.client.get_entity(dialog.entity)
group_info = {
'chat_id': str(entity.id),
'title': dialog.title or entity.title if hasattr(entity, 'title') else '',
'description': entity.about if hasattr(entity, 'about') else '',
'members_count': getattr(entity, 'participants_count', 0),
'is_channel': entity.broadcast if hasattr(entity, 'broadcast') else False,
'is_supergroup': entity.megagroup if hasattr(entity, 'megagroup') else False,
'username': entity.username if hasattr(entity, 'username') else '',
}
groups.append(group_info)
logger.info(f"{dialog.title}")
except Exception as e:
logger.warning(f" ⚠️ Ошибка при парсинге {dialog.title}: {e}")
continue
logger.info(f"✅ Получено {len(groups)} групп/каналов")
return groups
except Exception as e:
logger.error(f"❌ Ошибка при получении списка групп: {e}")
return groups
async def sync_group_to_db(self, chat_id: int) -> bool:
"""
Синхронизировать информацию о группе и участников в БД
Returns:
True если успешно, False иначе
"""
try:
# Получить информацию о группе
group_info = await self.parse_group_info(chat_id)
if not group_info:
logger.error(f"Не удалось получить информацию о группе {chat_id}")
return False
# Получить участников
members = await self.parse_group_members(chat_id)
# Сохранить в БД
async with AsyncSessionLocal() as session:
from app.database.repository import GroupRepository, GroupMemberRepository
group_repo = GroupRepository(session)
member_repo = GroupMemberRepository(session)
# Обновить информацию о группе
group_data = {
'chat_id': int(group_info['chat_id']),
'title': group_info['title'],
'description': group_info['description'],
'members_count': group_info['members_count'],
'is_active': True,
}
await group_repo.add_or_update_group(group_data)
logger.info(f"✅ Группа {group_info['title']} сохранена в БД")
# Сохранить участников
if members:
for member in members:
member_data = {
'group_id': chat_id,
'user_id': int(member['user_id']),
'username': member['username'],
'first_name': member['first_name'],
'last_name': member['last_name'],
'is_bot': member['is_bot'],
}
await member_repo.add_or_update_member(member_data)
logger.info(f"{len(members)} участников сохранено в БД")
await session.commit()
return True
except Exception as e:
logger.error(f"❌ Ошибка при синхронизации группы {chat_id}: {e}")
return False
# Глобальный экземпляр парсера
userbot_parser = UserbotParser()