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

View File

@@ -6,14 +6,15 @@ from telegram.ext import (
CommandHandler,
CallbackQueryHandler,
ChatMemberHandler,
ConversationHandler,
MessageHandler,
ConversationHandler,
filters,
)
from app.database import init_db
from app.handlers import (
start,
help_command,
sync_groups_command,
start_callback,
manage_messages,
manage_groups,
@@ -21,33 +22,60 @@ from app.handlers import (
list_groups,
send_message,
my_chat_member,
userbot_menu,
userbot_settings,
userbot_init,
userbot_collect_groups,
userbot_collect_members,
userbot_parse_members,
cancel_userbot,
)
from app.handlers.message_manager import (
create_message_start,
create_message_title,
create_message_text,
select_groups,
CREATE_MSG_TITLE,
CREATE_MSG_TEXT,
SELECT_GROUPS,
handle_message_input,
)
from app.handlers.telethon_client import telethon_manager
from app.utils.keyboards import CallbackType
from app.settings import Config
# Загружаем переменные окружения
load_dotenv()
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
level=logging.DEBUG
)
logger = logging.getLogger(__name__)
async def debug_update_handler(update, context):
"""Обработчик для отладки всех обновлений"""
logger.info(f"🎯 Получено обновление: {update}")
if update.message:
logger.info(f"📨 Сообщение от {update.effective_user.id}: {update.message.text}")
elif update.callback_query:
logger.info(f"🔘 Callback от {update.effective_user.id}: {update.callback_query.data}")
else:
logger.info(f"❓ Неизвестное обновление: {update.to_dict() if hasattr(update, 'to_dict') else str(update)}")
return None
# Получаем конфигурацию
if not Config.validate():
raise ValueError("❌ Конфигурация некорректна. Проверьте .env файл")
# Для UserBot контейнера: если есть TELETHON переменные и нет BOT_TOKEN - это ОК
is_userbot_only = (
os.getenv('TELETHON_API_ID')
and os.getenv('TELETHON_API_HASH')
and os.getenv('TELETHON_PHONE')
and not os.getenv('TELEGRAM_BOT_TOKEN')
)
if not is_userbot_only:
if not Config.validate():
raise ValueError("❌ Конфигурация некорректна. Проверьте .env файл")
async def main() -> None:
@@ -76,34 +104,44 @@ async def main() -> None:
# Создаем приложение
application = Application.builder().token(Config.TELEGRAM_BOT_TOKEN).build()
# Добавляем обработчики команд
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("help", help_command))
# Добавляем отладчик для всех текстовых сообщений (самый низкий приоритет)
application.add_handler(MessageHandler(filters.ALL, debug_update_handler), group=100)
# ConversationHandler для создания сообщения
create_message_handler = ConversationHandler(
entry_points=[CallbackQueryHandler(create_message_start, pattern=f"^{CallbackType.CREATE_MESSAGE}$")],
states={
CREATE_MSG_TITLE: [MessageHandler(filters.TEXT & ~filters.COMMAND, create_message_title)],
CREATE_MSG_TEXT: [MessageHandler(filters.TEXT & ~filters.COMMAND, create_message_text)],
SELECT_GROUPS: [CallbackQueryHandler(select_groups, pattern=r"^(select_group_\d+|done_groups|main_menu)$")],
},
fallbacks=[CommandHandler("cancel", start)],
)
application.add_handler(create_message_handler)
# Добавляем обработчики команд (высший приоритет, группа 0)
application.add_handler(CommandHandler("start", start), group=0)
application.add_handler(CommandHandler("help", help_command), group=0)
application.add_handler(CommandHandler("sync_groups", sync_groups_command), group=0)
# Добавляем обработчики callback'ов
application.add_handler(CallbackQueryHandler(start_callback, pattern=f"^{CallbackType.MAIN_MENU}$"))
application.add_handler(CallbackQueryHandler(manage_messages, pattern=f"^{CallbackType.MANAGE_MESSAGES}$"))
application.add_handler(CallbackQueryHandler(manage_groups, pattern=f"^{CallbackType.MANAGE_GROUPS}$"))
application.add_handler(CallbackQueryHandler(list_messages, pattern=f"^{CallbackType.LIST_MESSAGES}$"))
application.add_handler(CallbackQueryHandler(list_groups, pattern=f"^{CallbackType.LIST_GROUPS}$"))
# Добавляем обработчики callback'ов (группа 1)
application.add_handler(CallbackQueryHandler(start_callback, pattern=f"^{CallbackType.MAIN_MENU.value}$"), group=1)
application.add_handler(CallbackQueryHandler(manage_messages, pattern=f"^{CallbackType.MANAGE_MESSAGES.value}$"), group=1)
application.add_handler(CallbackQueryHandler(manage_groups, pattern=f"^{CallbackType.MANAGE_GROUPS.value}$"), group=1)
application.add_handler(CallbackQueryHandler(list_messages, pattern=f"^{CallbackType.LIST_MESSAGES.value}$"), group=1)
application.add_handler(CallbackQueryHandler(list_groups, pattern=f"^{CallbackType.LIST_GROUPS.value}$"), group=1)
application.add_handler(CallbackQueryHandler(send_message, pattern=r"^send_msg_\d+$"), group=1)
# CREATE_MESSAGE обрабатывается отдельным handler'ом с управлением состоянием
application.add_handler(CallbackQueryHandler(create_message_start, pattern=f"^{CallbackType.CREATE_MESSAGE.value}$"), group=1)
# Добавляем обработчик CallbackQuery для управления UserBot
application.add_handler(CallbackQueryHandler(userbot_menu, pattern="^userbot_menu$"), group=1)
application.add_handler(CallbackQueryHandler(userbot_settings, pattern="^userbot_settings$"), group=1)
application.add_handler(CallbackQueryHandler(userbot_init, pattern="^userbot_init$"), group=1)
application.add_handler(CallbackQueryHandler(userbot_collect_groups, pattern="^userbot_collect_groups$"), group=1)
application.add_handler(CallbackQueryHandler(userbot_collect_members, pattern="^userbot_collect_members$"), group=1)
application.add_handler(CallbackQueryHandler(userbot_parse_members, pattern=r"^userbot_members_\d+$"), group=1)
# Отправка сообщений
application.add_handler(CallbackQueryHandler(send_message, pattern=r"^send_msg_\d+$"))
# Добавляем обработчик для кнопки UserBot в главном меню
application.add_handler(CallbackQueryHandler(userbot_menu, pattern=f"^{CallbackType.MANAGE_USERBOT.value}$"), group=1)
# Обработчик добавления/удаления бота из групп
application.add_handler(ChatMemberHandler(my_chat_member, ChatMemberHandler.MY_CHAT_MEMBER))
# Select group callbacks
application.add_handler(CallbackQueryHandler(select_groups, pattern=r"^select_group_\d+$"), group=1)
application.add_handler(CallbackQueryHandler(select_groups, pattern=r"^done_groups$"), group=1)
# MessageHandler для текстового ввода (название и текст сообщения)
# Использует dispatch-функцию для маршрутизации в зависимости от состояния
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message_input), group=1)
# Обработчик добавления/удаления бота из групп (группа 3)
application.add_handler(ChatMemberHandler(my_chat_member, ChatMemberHandler.MY_CHAT_MEMBER), group=3)
# Запускаем бота
logger.info("🚀 Бот запущен. Ожидание команд...")

View File

@@ -11,6 +11,13 @@ if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
try:
# Используем nest_asyncio для избежания конфликтов event loop
try:
import nest_asyncio
nest_asyncio.apply()
except ImportError:
pass
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Бот остановлен пользователем")

View File

@@ -31,6 +31,46 @@ class GroupMemberRepository:
await self.session.flush()
return member
async def add_or_update_member(self, data: dict) -> GroupMember:
"""Добавить или обновить члена группы"""
group_id = data.get('group_id')
user_id = str(data.get('user_id'))
member = await self.get_member_by_user_id(group_id, user_id)
if member:
# Обновить существующего
if 'username' in data:
member.username = data['username']
if 'first_name' in data:
member.first_name = data['first_name']
if 'last_name' in data:
member.last_name = data['last_name']
if 'is_bot' in data:
member.is_bot = data['is_bot']
if 'is_admin' in data:
member.is_admin = data['is_admin']
if 'is_owner' in data:
member.is_owner = data['is_owner']
member.updated_at = datetime.utcnow()
else:
# Создать нового
member = GroupMember(
group_id=group_id,
user_id=user_id,
username=data.get('username'),
first_name=data.get('first_name'),
last_name=data.get('last_name'),
is_bot=data.get('is_bot', False),
is_admin=data.get('is_admin', False),
is_owner=data.get('is_owner', False),
joined_at=datetime.utcnow()
)
self.session.add(member)
await self.session.flush()
return member
async def get_member_by_user_id(self, group_id: int, user_id: str) -> Optional[GroupMember]:
"""Получить участника по user_id"""
result = await self.session.execute(

View File

@@ -24,6 +24,38 @@ class GroupRepository:
await self.session.refresh(group)
return group
async def add_or_update_group(self, data: dict) -> Group:
"""Добавить или обновить группу"""
chat_id = str(data.get('chat_id'))
group = await self.get_group_by_chat_id(chat_id)
if group:
# Обновить существующую
if 'title' in data:
group.title = data['title']
if 'description' in data:
group.description = data.get('description')
if 'members_count' in data:
group.members_count = data['members_count']
if 'slow_mode_delay' in data:
group.slow_mode_delay = data['slow_mode_delay']
group.updated_at = datetime.utcnow()
else:
# Создать новую
group = Group(
chat_id=chat_id,
title=data.get('title', ''),
slow_mode_delay=data.get('slow_mode_delay', 0)
)
if 'description' in data:
group.description = data['description']
if 'members_count' in data:
group.members_count = data['members_count']
self.session.add(group)
await self.session.flush()
return group
async def get_group_by_chat_id(self, chat_id: str) -> Optional[Group]:
"""Получить группу по ID чата"""
result = await self.session.execute(
@@ -38,6 +70,14 @@ class GroupRepository:
)
return result.scalars().all()
async def get_active_groups(self) -> List[Group]:
"""Получить все активные группы (alias)"""
return await self.get_all_active_groups()
async def get_group_by_id(self, group_id: int) -> Optional[Group]:
"""Получить группу по ID"""
return await self.session.get(Group, group_id)
async def update_group_slow_mode(self, group_id: int, delay: int) -> None:
"""Обновить slow mode задержку группы"""
group = await self.session.get(Group, group_id)
@@ -46,6 +86,17 @@ class GroupRepository:
group.updated_at = datetime.utcnow()
await self.session.commit()
async def update_group(self, group_id: int, title: str = None, slow_mode_delay: int = None) -> None:
"""Обновить информацию о группе"""
group = await self.session.get(Group, group_id)
if group:
if title is not None:
group.title = title
if slow_mode_delay is not None:
group.slow_mode_delay = slow_mode_delay
group.updated_at = datetime.utcnow()
await self.session.commit()
async def update_last_message_time(self, group_id: int) -> None:
"""Обновить время последнего сообщения"""
group = await self.session.get(Group, group_id)

View File

@@ -1,14 +1,20 @@
from .commands import start, help_command
from .commands import start, help_command, sync_groups_command
from .callbacks import (
start_callback, manage_messages, manage_groups,
list_messages, list_groups
)
from .sender import send_message
from .group_manager import my_chat_member
from .userbot_manager import (
userbot_menu, userbot_settings, userbot_init,
userbot_collect_groups, userbot_collect_members,
userbot_parse_members, cancel_userbot
)
__all__ = [
'start',
'help_command',
'sync_groups_command',
'start_callback',
'manage_messages',
'manage_groups',
@@ -16,4 +22,11 @@ __all__ = [
'list_groups',
'send_message',
'my_chat_member',
'userbot_menu',
'userbot_settings',
'userbot_init',
'userbot_collect_groups',
'userbot_collect_members',
'userbot_parse_members',
'cancel_userbot',
]

View File

@@ -1,5 +1,5 @@
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes, ConversationHandler
from telegram.ext import ContextTypes
from app.database import AsyncSessionLocal
from app.database.repository import GroupRepository, MessageRepository, MessageGroupRepository
from app.utils.keyboards import (
@@ -10,16 +10,11 @@ import logging
logger = logging.getLogger(__name__)
# Состояния для ConversationHandler
WAITING_MESSAGE_TEXT = 1
WAITING_MESSAGE_TITLE = 2
WAITING_GROUP_SELECTION = 3
WAITING_FOR_GROUP = 4
async def start_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Главное меню"""
query = update.callback_query
logger.info(f"🔘 Получена кнопка MAIN_MENU от пользователя {update.effective_user.id}")
await query.answer()
text = """🤖 <b>Автопостер - Главное меню</b>
@@ -35,50 +30,61 @@ async def start_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
async def manage_messages(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Меню управления сообщениями"""
query = update.callback_query
await query.answer()
text = """📨 <b>Управление сообщениями</b>
try:
query = update.callback_query
logger.info(f"🔘 Получена кнопка MANAGE_MESSAGES от пользователя {update.effective_user.id}")
await query.answer()
text = """📨 <b>Управление сообщениями</b>
Выберите действие:"""
keyboard = [
[InlineKeyboardButton(" Новое сообщение", callback_data=CallbackType.CREATE_MESSAGE)],
[InlineKeyboardButton("📜 Список сообщений", callback_data=CallbackType.LIST_MESSAGES)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU)],
]
keyboard = [
[InlineKeyboardButton(" Новое сообщение", callback_data=CallbackType.CREATE_MESSAGE.value)],
[InlineKeyboardButton("📜 Список сообщений", callback_data=CallbackType.LIST_MESSAGES.value)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU.value)],
]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
logger.info(f"✅ Сообщение обновлено для manage_messages")
except Exception as e:
logger.error(f"❌ Ошибка в manage_messages: {e}", exc_info=True)
async def manage_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Меню управления группами"""
query = update.callback_query
await query.answer()
text = """👥 <b>Управление группами</b>
try:
query = update.callback_query
logger.info(f"🔘 Получена кнопка MANAGE_GROUPS от пользователя {update.effective_user.id}")
await query.answer()
text = """👥 <b>Управление группами</b>
Выберите действие:"""
keyboard = [
[InlineKeyboardButton("📜 Список групп", callback_data=CallbackType.LIST_GROUPS)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU)],
]
keyboard = [
[InlineKeyboardButton("📜 Список групп", callback_data=CallbackType.LIST_GROUPS.value)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU.value)],
]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
logger.info(f"✅ Сообщение обновлено для manage_groups")
except Exception as e:
logger.error(f"❌ Ошибка в manage_groups: {e}", exc_info=True)
async def list_messages(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Список всех сообщений"""
query = update.callback_query
logger.info(f"🔘 Получена кнопка LIST_MESSAGES от пользователя {update.effective_user.id}")
await query.answer()
async with AsyncSessionLocal() as session:
@@ -87,7 +93,7 @@ async def list_messages(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
if not messages:
text = "📭 Нет сообщений"
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_MESSAGES)]]
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_MESSAGES.value)]]
else:
text = "📨 <b>Ваши сообщения:</b>\n\n"
keyboard = []
@@ -100,7 +106,7 @@ async def list_messages(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
InlineKeyboardButton("🗑️", callback_data=f"delete_msg_{msg.id}")
])
keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_MESSAGES)])
keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_MESSAGES.value)])
await query.edit_message_text(
text,
@@ -112,6 +118,7 @@ async def list_messages(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
async def list_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Список всех групп"""
query = update.callback_query
logger.info(f"🔘 Получена кнопка LIST_GROUPS от пользователя {update.effective_user.id}")
await query.answer()
async with AsyncSessionLocal() as session:
@@ -120,7 +127,7 @@ async def list_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
if not groups:
text = "👥 Нет групп в базе данных\n\nДобавьте бота в группы - они автоматически появятся здесь."
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_GROUPS)]]
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_GROUPS.value)]]
else:
text = "👥 <b>Группы в базе данных:</b>\n\n"
keyboard = []
@@ -137,7 +144,7 @@ async def list_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
InlineKeyboardButton("🗑️", callback_data=f"delete_group_{group.id}")
])
keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_GROUPS)])
keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MANAGE_GROUPS.value)])
await query.edit_message_text(
text,

View File

@@ -3,6 +3,8 @@ from telegram.ext import ContextTypes
from app.database import AsyncSessionLocal
from app.database.repository import GroupRepository, MessageRepository, MessageGroupRepository
from app.utils.keyboards import get_main_keyboard, get_groups_keyboard, get_messages_keyboard
from app.handlers.telethon_client import telethon_manager
from app.userbot.parser import userbot_parser
import logging
logger = logging.getLogger(__name__)
@@ -11,6 +13,7 @@ logger = logging.getLogger(__name__)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /start"""
user = update.effective_user
logger.info(f"📧 Получена команда /start от пользователя {user.id} (@{user.username})")
text = f"""👋 Привет, {user.first_name}!
@@ -32,6 +35,8 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработчик команды /help"""
user = update.effective_user
logger.info(f"📧 Получена команда /help от пользователя {user.id}")
text = """📖 Справка по использованию:
<b>Основные команды:</b>
@@ -56,3 +61,165 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
Нажмите /start для возврата в главное меню."""
await update.message.reply_text(text, parse_mode='HTML')
async def _sync_groups_with_userbot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Синхронизировать группы через новый UserBot парсер"""
try:
status_message = await update.message.reply_text(
"⏳ Синхронизирую группы через UserBot парсер..."
)
# Используем userbot для парсинга участников существующих групп
async with AsyncSessionLocal() as session:
repo = GroupRepository(session)
existing_groups = await repo.get_all_active_groups()
if not existing_groups:
await status_message.edit_text(
" В БД нет групп для синхронизации участников.\n\n"
"Сначала добавьте группы через /start → Группы"
)
return
synced_count = 0
for group in existing_groups:
try:
# Синхронизировать информацию о группе и участников
success = await userbot_parser.sync_group_to_db(int(group.chat_id))
if success:
synced_count += 1
logger.info(f"✅ Синхронизирована группа: {group.title}")
except Exception as e:
logger.error(f"❌ Ошибка при синхронизации {group.title}: {e}")
await session.commit()
result_text = f"""✅ <b>Синхронизация завершена!</b>
📊 Результаты:
• 🔄 Синхронизировано: {synced_count} групп
Информация о участниках обновлена и сохранена в БД!"""
await status_message.edit_text(result_text, parse_mode='HTML')
logger.info(f"✅ Синхронизация участников завершена: {synced_count} групп")
except Exception as e:
logger.error(f"❌ Ошибка при синхронизации через UserBot: {e}", exc_info=True)
await update.message.reply_text(
f"❌ Ошибка при синхронизации: {str(e)}"
)
async def sync_groups_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Синхронизировать группы из Telethon UserBot или UserBot парсера"""
user = update.effective_user
logger.info(f"🔄 Получена команда /sync_groups от пользователя {user.id}")
# Попытаться инициализировать userbot если еще не инициализирован
if not userbot_parser.is_initialized:
logger.info("📱 Инициализация UserBot парсера...")
init_success = await userbot_parser.initialize()
if not init_success:
logger.warning("⚠️ UserBot парсер не инициализирован, используем старый telethon_manager")
# Попытаться использовать новый userbot сначала
if userbot_parser.is_initialized:
logger.info("✅ Используем новый UserBot парсер")
return await _sync_groups_with_userbot(update, context)
else:
logger.info(" Используем старый Telethon клиент")
return await _sync_groups_with_telethon(update, context)
async def _sync_groups_with_telethon(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Синхронизировать группы через telethon_manager"""
user = update.effective_user
logger.info(f"🔄 Синхронизация через telethon_manager")
# Проверим, инициализирован ли Telethon клиент
if not telethon_manager.is_connected():
logger.warning("⚠️ Telethon клиент не инициализирован")
await update.message.reply_text(
"❌ Telethon UserBot не инициализирован.\n\n"
"Убедитесь, что переменные окружения TELETHON_API_ID и TELETHON_API_HASH установлены."
)
return
try:
# Отправим уведомление о начале синхронизации
status_message = await update.message.reply_text(
"⏳ Синхронизирую группы через Telethon UserBot..."
)
# Получить все группы от Telethon
groups = await telethon_manager.get_user_groups()
if not groups:
await status_message.edit_text(
" UserBot не найден ни в одной группе.\n\n"
"Чтобы добавить группы:\n"
"1. Пригласите UserBot в групп\u044b\n"
"2. Повторите /sync_groups"
)
return
# Сохранить группы в БД
added_count = 0
updated_count = 0
async with AsyncSessionLocal() as session:
repo = GroupRepository(session)
for group in groups:
try:
# Конвертировать chat_id в string
chat_id_str = str(group['chat_id'])
# Попытаться найти существующую группу
existing = await repo.get_group_by_chat_id(chat_id_str)
if existing:
# Обновить информацию
await repo.update_group(
existing.id,
title=group['title'],
slow_mode_delay=group['slow_mode_delay']
)
updated_count += 1
logger.info(f"✏️ Обновлена группа: {group['title']} (ID: {chat_id_str})")
else:
# Добавить новую группу
await repo.add_group(
chat_id=chat_id_str,
title=group['title'],
slow_mode_delay=group['slow_mode_delay']
)
added_count += 1
logger.info(f"✅ Добавлена группа: {group['title']} (ID: {chat_id_str})")
except Exception as e:
logger.error(f"❌ Ошибка при добавлении группы {group['title']}: {e}")
continue
await session.commit()
# Отправить результаты
result_text = f"""✅ <b>Синхронизация завершена!</b>
📊 Результаты:
Добавлено: {added_count}
• ✏️ Обновлено: {updated_count}
• 📈 Всего в БД: {added_count + updated_count}
Теперь вы можете использовать эти группы для отправки сообщений!"""
await status_message.edit_text(result_text, parse_mode='HTML')
logger.info(f"✅ Синхронизация завершена: добавлено {added_count}, обновлено {updated_count}")
except Exception as e:
logger.error(f"❌ Ошибка при синхронизации групп: {e}", exc_info=True)
await update.message.reply_text(
f"❌ Ошибка при синхронизации: {str(e)}"
)

View File

@@ -1,5 +1,5 @@
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes, ConversationHandler
from telegram.ext import ContextTypes
from app.database import AsyncSessionLocal
from app.database.repository import (
GroupRepository, MessageRepository, MessageGroupRepository
@@ -11,34 +11,57 @@ import logging
logger = logging.getLogger(__name__)
# Состояния для ConversationHandler
CREATE_MSG_TITLE = 1
CREATE_MSG_TEXT = 2
SELECT_GROUPS = 3
WAITING_GROUP_INPUT = 4
# Состояния (теперь управляются через context.user_data)
STATE_WAITING_MSG_TITLE = "waiting_title"
STATE_WAITING_MSG_TEXT = "waiting_text"
STATE_SELECTING_GROUPS = "selecting_groups"
async def create_message_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
async def handle_message_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Распределяет текстовый ввод в зависимости от текущего состояния"""
state = context.user_data.get('message_state')
if state == STATE_WAITING_MSG_TITLE:
await create_message_title(update, context)
elif state == STATE_WAITING_MSG_TEXT:
await create_message_text(update, context)
# Если не в состоянии ввода - игнорируем
async def create_message_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Начало создания нового сообщения"""
query = update.callback_query
logger.info(f"📝 Начало создания сообщения от пользователя {update.effective_user.id}")
await query.answer()
# Инициализируем состояние
context.user_data['message_state'] = STATE_WAITING_MSG_TITLE
context.user_data['message_title'] = None
context.user_data['message_text'] = None
context.user_data['selected_groups'] = set()
text = "📝 Введите название сообщения (короткое описание):"
await query.edit_message_text(text)
return CREATE_MSG_TITLE
async def create_message_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
async def create_message_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Получаем название и просим текст"""
message = update.message
# Проверяем что мы в правильном состоянии
if context.user_data.get('message_state') != STATE_WAITING_MSG_TITLE:
return
logger.info(f"📝 Получено название сообщения: {message.text[:50]}")
title = message.text.strip()
if len(title) > 100:
await message.reply_text("❌ Название слишком длинное (макс 100 символов)")
return CREATE_MSG_TITLE
return
context.user_data['message_title'] = title
context.user_data['message_state'] = STATE_WAITING_MSG_TEXT
text = """✏️ Теперь введите текст сообщения.
@@ -51,22 +74,28 @@ async def create_message_title(update: Update, context: ContextTypes.DEFAULT_TYP
Введите /cancel для отмены"""
await message.reply_text(text, parse_mode='HTML')
return CREATE_MSG_TEXT
async def create_message_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
async def create_message_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Получаем текст и показываем выбор групп"""
message = update.message
# Проверяем что мы в правильном состоянии
if context.user_data.get('message_state') != STATE_WAITING_MSG_TEXT:
return
logger.info(f"📝 Получен текст сообщения от пользователя {update.effective_user.id}")
if message.text == '/cancel':
context.user_data['message_state'] = None
await message.reply_text("❌ Отменено", reply_markup=get_main_keyboard())
return ConversationHandler.END
return
text = message.text.strip()
if len(text) > 4096:
await message.reply_text("❌ Текст слишком длинный (макс 4096 символов)")
return CREATE_MSG_TEXT
return
context.user_data['message_text'] = text
@@ -85,23 +114,24 @@ async def create_message_text(update: Update, context: ContextTypes.DEFAULT_TYPE
groups = await group_repo.get_all_active_groups()
if not groups:
context.user_data['message_state'] = None
await message.reply_text(
"❌ Нет активных групп. Сначала добавьте бота в группы.",
reply_markup=get_main_keyboard()
)
return ConversationHandler.END
return
# Создаем клавиатуру с группами
keyboard = []
for group in groups:
callback = f"select_group_{group.id}"
keyboard.append([InlineKeyboardButton(
f" {group.title} (delay: {group.slow_mode_delay}s)",
f" {group.title} (delay: {group.slow_mode_delay}s)",
callback_data=callback
)])
keyboard.append([InlineKeyboardButton("✔️ Готово", callback_data="done_groups")])
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data=f"{CallbackType.MAIN_MENU}")])
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data=CallbackType.MAIN_MENU.value)])
text = f"""✅ Сообщение создано: <b>{context.user_data['message_title']}</b>
@@ -113,22 +143,29 @@ async def create_message_text(update: Update, context: ContextTypes.DEFAULT_TYPE
reply_markup=InlineKeyboardMarkup(keyboard)
)
context.user_data['selected_groups'] = []
return SELECT_GROUPS
context.user_data['selected_groups'] = set()
context.user_data['message_state'] = STATE_SELECTING_GROUPS
async def select_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
async def select_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Выбор групп для отправки"""
query = update.callback_query
callback_data = query.data
# Проверяем что мы в правильном состоянии
if context.user_data.get('message_state') != STATE_SELECTING_GROUPS:
return
logger.info(f"🔘 Получен callback select_groups: {callback_data} от пользователя {update.effective_user.id}")
await query.answer()
if callback_data == "done_groups":
# Подтверждаем выбор
selected = context.user_data.get('selected_groups', [])
selected = context.user_data.get('selected_groups', set())
if not selected:
await query.answer("❌ Выберите хотя бы одну группу", show_alert=True)
return SELECT_GROUPS
return
# Добавляем сообщение в выбранные группы
message_id = context.user_data['message_id']
@@ -145,17 +182,18 @@ async def select_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i
Теперь вы можете отправить сообщение нажав кнопку "Отправить" в списке сообщений."""
context.user_data['message_state'] = None
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_main_keyboard())
return ConversationHandler.END
return
elif callback_data.startswith("select_group_"):
group_id = int(callback_data.split("_")[2])
selected = context.user_data.get('selected_groups', [])
selected = context.user_data.get('selected_groups', set())
if group_id in selected:
selected.remove(group_id)
selected.discard(group_id)
else:
selected.append(group_id)
selected.add(group_id)
context.user_data['selected_groups'] = selected
@@ -175,7 +213,7 @@ async def select_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i
)])
keyboard.append([InlineKeyboardButton("✔️ Готово", callback_data="done_groups")])
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data=f"{CallbackType.MAIN_MENU}")])
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data=CallbackType.MAIN_MENU.value)])
await query.edit_message_text(
f"Выбрано групп: {len(selected)}",

View File

@@ -2,7 +2,8 @@ import logging
import os
from typing import List, Optional, Dict
from telethon import TelegramClient, events
from telethon.tl.types import ChatMember, User
from telethon.tl.types import User
from telethon.tl.types.auth import Authorization
from telethon.errors import (
FloodWaitError, UserDeactivatedError, ChatAdminRequiredError,
PeerIdInvalidError, ChannelInvalidError, UserNotParticipantError
@@ -272,6 +273,50 @@ class TelethonClientManager:
logger.error(f"❌ Ошибка при поиске сообщений: {e}")
return []
async def get_user_groups(self) -> List[Dict]:
"""
Получить все группы и супергруппы пользователя
Returns:
List[Dict]: Список групп с информацией {id, title, slow_mode_delay, members_count}
"""
if not self.is_initialized:
logger.error("Telethon клиент не инициализирован")
return []
try:
groups = []
from telethon.tl.types import Chat, Channel
# Получить все диалоги (чаты/группы)
async for dialog in self.client.iter_dialogs():
entity = dialog.entity
# Пропустить личные чаты и каналы (только группы и супергруппы)
if not isinstance(entity, (Chat, Channel)):
continue
# Пропустить каналы (broadcast)
if isinstance(entity, Channel) and entity.broadcast:
continue
group_info = {
'chat_id': entity.id,
'title': entity.title if hasattr(entity, 'title') else str(entity.id),
'slow_mode_delay': entity.slowmode_seconds if hasattr(entity, 'slowmode_seconds') else 0,
'members_count': entity.participants_count if hasattr(entity, 'participants_count') else 0,
'is_supergroup': isinstance(entity, Channel), # Channel = supergroup/megagroup
}
groups.append(group_info)
logger.debug(f"📍 Найдена группа: {group_info['title']} (ID: {group_info['chat_id']})")
logger.info(f"✅ Получено {len(groups)} групп от Telethon")
return groups
except Exception as e:
logger.error(f"❌ Ошибка при получении групп: {e}")
return []
def is_connected(self) -> bool:
"""Проверить, подключен ли клиент"""
return self.is_initialized and self.client is not None

View File

@@ -0,0 +1,449 @@
"""
Обработчик управления UserBot для сбора групп и участников
"""
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes, ConversationHandler
from app.userbot.parser import userbot_parser
from app.database import AsyncSessionLocal
from app.database.repository import GroupRepository
from app.database.member_repository import GroupMemberRepository
from app.utils.keyboards import CallbackType
import logging
logger = logging.getLogger(__name__)
# Состояния для ConversationHandler
USERBOT_MENU = 1
USERBOT_SETTINGS = 2
USERBOT_COLLECTING_GROUPS = 3
USERBOT_SELECT_GROUP = 4
USERBOT_COLLECTING_MEMBERS = 5
async def userbot_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Меню управления UserBot"""
try:
query = update.callback_query
if query:
await query.answer()
text = """🤖 <b>UserBot - Управление парсингом</b>
Что вы хотите сделать?
<i>UserBot собирает информацию о группах и их участниках от имени пользователя</i>"""
keyboard = [
[InlineKeyboardButton("⚙️ Настройки", callback_data="userbot_settings")],
[InlineKeyboardButton("📥 Собрать группы", callback_data="userbot_collect_groups")],
[InlineKeyboardButton("👥 Собрать участников", callback_data="userbot_collect_members")],
[InlineKeyboardButton("⬅️ Назад в меню", callback_data=CallbackType.MAIN_MENU.value)],
]
if query:
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
else:
await update.message.reply_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_MENU
except Exception as e:
logger.error(f"❌ Ошибка в userbot_menu: {e}", exc_info=True)
return ConversationHandler.END
async def userbot_settings(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Настройки UserBot"""
try:
query = update.callback_query
await query.answer()
# Проверяем статус UserBot
is_initialized = userbot_parser.is_initialized
status = "✅ Инициализирован" if is_initialized else "Не инициализирован"
text = f"""⚙️ <b>Настройки UserBot</b>
<b>Статус:</b> {status}
<b>Возможности:</b>
• Собирать информацию о группах
• Собирать списки участников
• Сохранять данные в БД
• Работать в фоне через Celery
Нажмите кнопку для продолжения"""
keyboard = [
[InlineKeyboardButton("🔄 Инициализировать", callback_data="userbot_init")],
[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")],
]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_SETTINGS
except Exception as e:
logger.error(f"❌ Ошибка в userbot_settings: {e}", exc_info=True)
return ConversationHandler.END
async def userbot_init(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Инициализация UserBot"""
try:
query = update.callback_query
await query.answer()
await query.edit_message_text(
"⏳ Инициализирую UserBot...",
parse_mode='HTML'
)
# Инициализируем UserBot
success = await userbot_parser.initialize()
if success:
text = """✅ <b>UserBot успешно инициализирован!</b>
Теперь вы можете:
• Собирать информацию о группах
• Собирать списки участников
• Синхронизировать данные в БД
Перейдите в меню для продолжения."""
keyboard = [[InlineKeyboardButton("🔄 Меню", callback_data="userbot_menu")]]
else:
text = """❌ <b>Ошибка инициализации UserBot</b>
Убедитесь, что:
• Переменные окружения установлены
• TELETHON_API_ID и TELETHON_API_HASH верные
• Сессия сохранена в sessions/userbot_session.session
Попробуйте позже."""
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_settings")]]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_SETTINGS
except Exception as e:
logger.error(f"❌ Ошибка инициализации UserBot: {e}", exc_info=True)
text = f"""❌ <b>Ошибка при инициализации:</b>
<code>{str(e)[:200]}</code>
Проверьте логи бота."""
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_settings")]]
query = update.callback_query
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_SETTINGS
async def userbot_collect_groups(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Сбор групп от пользователя"""
try:
query = update.callback_query
await query.answer()
if not userbot_parser.is_initialized:
text = "❌ UserBot не инициализирован!\n\nПерейдите в настройки для инициализации."
keyboard = [[InlineKeyboardButton("⚙️ Настройки", callback_data="userbot_settings")]]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_MENU
await query.edit_message_text(
"⏳ <b>Собираю информацию о группах...</b>\n\nЭто может занять некоторое время...",
parse_mode='HTML'
)
# Собираем группы
logger.info(f"📥 Начало сбора групп для пользователя {update.effective_user.id}")
groups = await userbot_parser.parse_groups_user_in()
if not groups:
text = "❌ <b>Не найдено групп</b>\n\nУбедитесь, что вы состоите в группах."
keyboard = [[InlineKeyboardButton("🔄 Попробовать снова", callback_data="userbot_collect_groups")],
[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")]]
else:
# Сохраняем группы в контексте
context.user_data['available_groups'] = groups
text = f"""✅ <b>Найдено групп: {len(groups)}</b>
<b>Список групп:</b>"""
for i, group in enumerate(groups, 1):
title = group.get('title', 'Неизвестная группа')
chat_id = group.get('chat_id', 'Unknown')
members = group.get('members_count', 0)
text += f"\n{i}. {title}\n 👥 Участников: {members}"
text += "\n\n💾 Группы сохранены в базе данных!"
# Сохраняем группы в БД
async with AsyncSessionLocal() as session:
repo = GroupRepository(session)
for group in groups:
await repo.add_or_update_group({
'chat_id': group.get('chat_id'),
'title': group.get('title'),
'description': group.get('description', ''),
'members_count': group.get('members_count', 0),
'is_active': True
})
logger.info(f"✅ Сохранено {len(groups)} групп")
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")]]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_COLLECTING_GROUPS
except Exception as e:
logger.error(f"❌ Ошибка при сборе групп: {e}", exc_info=True)
text = f"""❌ <b>Ошибка при сборе групп:</b>
<code>{str(e)[:200]}</code>"""
keyboard = [[InlineKeyboardButton("🔄 Попробовать снова", callback_data="userbot_collect_groups")],
[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")]]
query = update.callback_query
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_COLLECTING_GROUPS
async def userbot_collect_members(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Выбор группы для сбора участников"""
try:
query = update.callback_query
await query.answer()
if not userbot_parser.is_initialized:
text = "❌ UserBot не инициализирован!\n\nПерейдите в настройки для инициализации."
keyboard = [[InlineKeyboardButton("⚙️ Настройки", callback_data="userbot_settings")]]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_MENU
# Получаем группы из БД
async with AsyncSessionLocal() as session:
repo = GroupRepository(session)
groups = await repo.get_active_groups()
if not groups:
text = "❌ <b>Не найдено активных групп</b>\n\nСначала соберите информацию о группах."
keyboard = [[InlineKeyboardButton("📥 Собрать группы", callback_data="userbot_collect_groups")],
[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")]]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_SELECT_GROUP
text = """👥 <b>Выберите группу для сбора участников</b>
Нажмите на группу для сбора списка участников:"""
keyboard = []
for group in groups:
title = group.title if hasattr(group, 'title') else group.get('title', 'Unknown')
group_id = group.id if hasattr(group, 'id') else group.get('id', 0)
callback_data = f"userbot_members_{group_id}"
keyboard.append([InlineKeyboardButton(f"👥 {title}", callback_data=callback_data)])
keyboard.append([InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")])
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_SELECT_GROUP
except Exception as e:
logger.error(f"❌ Ошибка при выборе группы: {e}", exc_info=True)
text = f"""❌ <b>Ошибка:</b>
<code>{str(e)[:200]}</code>"""
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_menu")]]
query = update.callback_query
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_SELECT_GROUP
async def userbot_parse_members(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Сбор участников для выбранной группы"""
try:
query = update.callback_query
await query.answer()
# Извлекаем group_id из callback_data
group_id = int(query.data.replace("userbot_members_", ""))
await query.edit_message_text(
f"⏳ <b>Собираю участников группы...</b>\n\ngroup_id: {group_id}\n\nЭто может занять некоторое время...",
parse_mode='HTML'
)
# Получаем информацию о группе из БД
async with AsyncSessionLocal() as session:
repo = GroupRepository(session)
group = await repo.get_group_by_id(group_id)
if not group:
text = "❌ Группа не найдена в базе данных."
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_collect_members")]]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_COLLECTING_MEMBERS
chat_id = group.chat_id if hasattr(group, 'chat_id') else group.get('chat_id')
title = group.title if hasattr(group, 'title') else group.get('title', 'Unknown')
logger.info(f"👥 Начало сбора участников для группы {title} (chat_id: {chat_id})")
# Собираем участников
members = await userbot_parser.parse_group_members(chat_id=chat_id, limit=10000)
if not members:
text = f"❌ <b>Не удалось собрать участников группы:</b>\n{title}"
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_collect_members")]]
else:
# Сохраняем участников в БД
async with AsyncSessionLocal() as session:
member_repo = GroupMemberRepository(session)
for member in members:
await member_repo.add_or_update_member({
'group_id': group_id,
'user_id': member.get('user_id'),
'username': member.get('username'),
'first_name': member.get('first_name'),
'last_name': member.get('last_name'),
'is_bot': member.get('is_bot', False),
'is_admin': member.get('is_admin', False),
'is_owner': member.get('is_owner', False),
})
# Статистика
total = len(members)
bots = len([m for m in members if m.get('is_bot')])
admins = len([m for m in members if m.get('is_admin')])
owners = len([m for m in members if m.get('is_owner')])
text = f"""✅ <b>Участники собраны!</b>
<b>Группа:</b> {title}
<b>Статистика:</b>
• 👥 Всего участников: {total}
• 🤖 Ботов: {bots}
• 👑 Администраторов: {admins}
• 🔑 Владельцев: {owners}
💾 Данные сохранены в базе данных!"""
logger.info(f"✅ Сохранено {total} участников группы {title}")
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_collect_members")]]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_COLLECTING_MEMBERS
except Exception as e:
logger.error(f"❌ Ошибка при сборе участников: {e}", exc_info=True)
text = f"""❌ <b>Ошибка при сборе участников:</b>
<code>{str(e)[:200]}</code>
Это может быть вызвано:
• FloodWait от Telegram
• Недостатком прав доступа
• Проблемой с сессией UserBot"""
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data="userbot_collect_members")],
[InlineKeyboardButton("🏠 Меню", callback_data="userbot_menu")]]
query = update.callback_query
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return USERBOT_COLLECTING_MEMBERS
async def cancel_userbot(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Отмена диалога"""
query = update.callback_query
if query:
await query.answer()
keyboard = [[InlineKeyboardButton("🏠 Главное меню", callback_data=CallbackType.MAIN_MENU.value)]]
await query.edit_message_text(
"❌ Диалог отменен",
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return ConversationHandler.END

Binary file not shown.

View File

@@ -20,7 +20,8 @@ class Config:
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
TELEGRAM_TIMEOUT = int(os.getenv('TELEGRAM_TIMEOUT', '30'))
if not TELEGRAM_BOT_TOKEN:
# Не требовать BOT_TOKEN если запущены в режиме UserBot микросервиса
if not TELEGRAM_BOT_TOKEN and not os.getenv('TELETHON_API_ID'):
raise ValueError(
"❌ TELEGRAM_BOT_TOKEN не установлен в .env\n"
"Получите токен у @BotFather в Telegram"
@@ -130,7 +131,12 @@ class Config:
@classmethod
def validate(cls) -> bool:
"""Проверить конфигурацию"""
# Основное - токен бота
# Если есть TELETHON_API_ID - это UserBot микросервис, BOT_TOKEN не требуется
if cls.TELETHON_API_ID:
# Проверить конфиг Telethon
return bool(cls.TELETHON_API_ID and cls.TELETHON_API_HASH and cls.TELETHON_PHONE)
# Для основного бота - требуется токен
if not cls.TELEGRAM_BOT_TOKEN:
return False

1
app/userbot/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Telethon UserBot Microservice

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

139
app/userbot/tasks.py Normal file
View File

@@ -0,0 +1,139 @@
"""
Celery задачи для UserBot микросервиса
Запускает парсинг групп в фоновом режиме
"""
import asyncio
import logging
from celery import shared_task
from app.userbot.parser import userbot_parser
from app.database import AsyncSessionLocal
from app.database.repository import GroupRepository
logger = logging.getLogger(__name__)
def run_async(coro):
"""Вспомогательная функция для запуска async функций в Celery"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
return loop.run_until_complete(coro)
finally:
loop.close()
@shared_task(name='app.userbot.tasks.initialize_userbot')
def initialize_userbot_task():
"""Инициализировать UserBot при запуске"""
logger.info("🚀 Инициализация UserBot...")
result = run_async(userbot_parser.initialize())
if result:
logger.info("✅ UserBot успешно инициализирован")
return {'status': 'success', 'message': 'UserBot initialized'}
else:
logger.error("❌ Ошибка инициализации UserBot")
return {'status': 'error', 'message': 'UserBot initialization failed'}
@shared_task(name='app.userbot.tasks.parse_group')
def parse_group_task(chat_id: int):
"""
Парсить группу и сохранить в БД
Args:
chat_id: ID группы для парсинга
"""
logger.info(f"📊 Парсинг группы {chat_id}...")
result = run_async(userbot_parser.sync_group_to_db(chat_id))
if result:
logger.info(f"✅ Группа {chat_id} успешно спарсена")
return {'status': 'success', 'chat_id': chat_id, 'message': 'Group parsed successfully'}
else:
logger.error(f"❌ Ошибка парсинга группы {chat_id}")
return {'status': 'error', 'chat_id': chat_id, 'message': 'Group parsing failed'}
@shared_task(name='app.userbot.tasks.sync_all_groups')
def sync_all_groups_task():
"""Синхронизировать все активные группы из БД"""
logger.info("🔄 Начало синхронизации всех групп...")
async def _sync_all():
try:
async with AsyncSessionLocal() as session:
repo = GroupRepository(session)
groups = await repo.get_all_active_groups()
if not groups:
logger.info(" Нет активных групп для синхронизации")
return {'status': 'success', 'groups_synced': 0}
synced = 0
failed = 0
for group in groups:
success = await userbot_parser.sync_group_to_db(group.chat_id)
if success:
synced += 1
else:
failed += 1
logger.info(f"✅ Синхронизировано {synced} групп (ошибок: {failed})")
return {'status': 'success', 'groups_synced': synced, 'groups_failed': failed}
except Exception as e:
logger.error(f"❌ Ошибка при синхронизации групп: {e}")
return {'status': 'error', 'message': str(e)}
return run_async(_sync_all())
@shared_task(name='app.userbot.tasks.parse_group_members')
def parse_group_members_task(chat_id: int, limit: int = 10000):
"""
Парсить участников группы
Args:
chat_id: ID группы
limit: максимум участников
"""
logger.info(f"👥 Парсинг участников группы {chat_id} (лимит: {limit})...")
async def _parse_members():
try:
members = await userbot_parser.parse_group_members(chat_id, limit)
if not members:
return {'status': 'error', 'chat_id': chat_id, 'members_count': 0}
# Сохранить в БД
async with AsyncSessionLocal() as session:
from app.database.repository import GroupMemberRepository
member_repo = GroupMemberRepository(session)
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)
await session.commit()
logger.info(f"{len(members)} участников группы {chat_id} сохранено в БД")
return {'status': 'success', 'chat_id': chat_id, 'members_count': len(members)}
except Exception as e:
logger.error(f"❌ Ошибка при парсинге участников {chat_id}: {e}")
return {'status': 'error', 'chat_id': chat_id, 'message': str(e)}
return run_async(_parse_members())

View File

@@ -6,6 +6,7 @@ class CallbackType(str, Enum):
"""Типы callback'ов для кнопок"""
MANAGE_MESSAGES = "manage_messages"
MANAGE_GROUPS = "manage_groups"
MANAGE_USERBOT = "manage_userbot"
CREATE_MESSAGE = "create_message"
CREATE_GROUP = "create_group"
VIEW_MESSAGE = "view_message"
@@ -25,8 +26,11 @@ def get_main_keyboard() -> InlineKeyboardMarkup:
"""Главное меню"""
keyboard = [
[
InlineKeyboardButton("📨 Сообщения", callback_data=CallbackType.MANAGE_MESSAGES),
InlineKeyboardButton("👥 Группы", callback_data=CallbackType.MANAGE_GROUPS),
InlineKeyboardButton("📨 Сообщения", callback_data=CallbackType.MANAGE_MESSAGES.value),
InlineKeyboardButton("👥 Группы", callback_data=CallbackType.MANAGE_GROUPS.value),
],
[
InlineKeyboardButton("🤖 UserBot", callback_data=CallbackType.MANAGE_USERBOT.value),
]
]
return InlineKeyboardMarkup(keyboard)
@@ -35,9 +39,9 @@ def get_main_keyboard() -> InlineKeyboardMarkup:
def get_messages_keyboard() -> InlineKeyboardMarkup:
"""Меню управления сообщениями"""
keyboard = [
[InlineKeyboardButton(" Новое сообщение", callback_data=CallbackType.CREATE_MESSAGE)],
[InlineKeyboardButton("📜 Список сообщений", callback_data=CallbackType.LIST_MESSAGES)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU)],
[InlineKeyboardButton(" Новое сообщение", callback_data=CallbackType.CREATE_MESSAGE.value)],
[InlineKeyboardButton("📜 Список сообщений", callback_data=CallbackType.LIST_MESSAGES.value)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU.value)],
]
return InlineKeyboardMarkup(keyboard)
@@ -45,16 +49,16 @@ def get_messages_keyboard() -> InlineKeyboardMarkup:
def get_groups_keyboard() -> InlineKeyboardMarkup:
"""Меню управления группами"""
keyboard = [
[InlineKeyboardButton(" Добавить группу", callback_data=CallbackType.CREATE_GROUP)],
[InlineKeyboardButton("📜 Список групп", callback_data=CallbackType.LIST_GROUPS)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU)],
[InlineKeyboardButton(" Добавить группу", callback_data=CallbackType.CREATE_GROUP.value)],
[InlineKeyboardButton("📜 Список групп", callback_data=CallbackType.LIST_GROUPS.value)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU.value)],
]
return InlineKeyboardMarkup(keyboard)
def get_back_keyboard() -> InlineKeyboardMarkup:
"""Кнопка назад"""
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU)]]
keyboard = [[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.MAIN_MENU.value)]]
return InlineKeyboardMarkup(keyboard)
@@ -63,7 +67,7 @@ def get_message_actions_keyboard(message_id: int) -> InlineKeyboardMarkup:
keyboard = [
[InlineKeyboardButton("📤 Отправить", callback_data=f"send_msg_{message_id}")],
[InlineKeyboardButton("🗑️ Удалить", callback_data=f"delete_msg_{message_id}")],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.LIST_MESSAGES)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.LIST_MESSAGES.value)],
]
return InlineKeyboardMarkup(keyboard)
@@ -73,7 +77,7 @@ def get_group_actions_keyboard(group_id: int) -> InlineKeyboardMarkup:
keyboard = [
[InlineKeyboardButton("📝 Сообщения", callback_data=f"group_messages_{group_id}")],
[InlineKeyboardButton("🗑️ Удалить", callback_data=f"delete_group_{group_id}")],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.LIST_GROUPS)],
[InlineKeyboardButton("⬅️ Назад", callback_data=CallbackType.LIST_GROUPS.value)],
]
return InlineKeyboardMarkup(keyboard)
@@ -83,7 +87,7 @@ def get_yes_no_keyboard(action: str) -> InlineKeyboardMarkup:
keyboard = [
[
InlineKeyboardButton("✅ Да", callback_data=f"confirm_{action}"),
InlineKeyboardButton("❌ Нет", callback_data=CallbackType.MAIN_MENU),
InlineKeyboardButton("❌ Нет", callback_data=CallbackType.MAIN_MENU.value),
]
]
return InlineKeyboardMarkup(keyboard)