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

@@ -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)}",