Files
TG_autoposter/app/userbot/parser.py
Andrew K. Choi 48f8c6f0eb 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
2025-12-21 12:09:11 +09:00

276 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()