✅ 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:
100
app/__init__.py
100
app/__init__.py
@@ -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("🚀 Бот запущен. Ожидание команд...")
|
||||
|
||||
@@ -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("Бот остановлен пользователем")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}"
|
||||
)
|
||||
|
||||
@@ -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)}",
|
||||
|
||||
@@ -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
|
||||
|
||||
449
app/handlers/userbot_manager.py
Normal file
449
app/handlers/userbot_manager.py
Normal 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
|
||||
BIN
app/sessions/telethon_session.session
Normal file
BIN
app/sessions/telethon_session.session
Normal file
Binary file not shown.
@@ -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
1
app/userbot/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Telethon UserBot Microservice
|
||||
275
app/userbot/parser.py
Normal file
275
app/userbot/parser.py
Normal 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
139
app/userbot/tasks.py
Normal 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())
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user