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
237 lines
9.0 KiB
Python
237 lines
9.0 KiB
Python
from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
|
||
from telegram.ext import ContextTypes
|
||
from app.database import AsyncSessionLocal
|
||
from app.database.repository import (
|
||
GroupRepository, MessageRepository, MessageGroupRepository
|
||
)
|
||
from app.utils.keyboards import (
|
||
get_back_keyboard, get_main_keyboard, CallbackType
|
||
)
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Состояния (теперь управляются через context.user_data)
|
||
STATE_WAITING_MSG_TITLE = "waiting_title"
|
||
STATE_WAITING_MSG_TEXT = "waiting_text"
|
||
STATE_SELECTING_GROUPS = "selecting_groups"
|
||
|
||
|
||
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)
|
||
|
||
|
||
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
|
||
|
||
context.user_data['message_title'] = title
|
||
context.user_data['message_state'] = STATE_WAITING_MSG_TEXT
|
||
|
||
text = """✏️ Теперь введите текст сообщения.
|
||
|
||
Вы можете использовать HTML форматирование:
|
||
<b>жирный</b>
|
||
<i>курсив</i>
|
||
<u>подчеркивание</u>
|
||
<code>код</code>
|
||
|
||
Введите /cancel для отмены"""
|
||
|
||
await message.reply_text(text, parse_mode='HTML')
|
||
|
||
|
||
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
|
||
|
||
text = message.text.strip()
|
||
|
||
if len(text) > 4096:
|
||
await message.reply_text("❌ Текст слишком длинный (макс 4096 символов)")
|
||
return
|
||
|
||
context.user_data['message_text'] = text
|
||
|
||
# Сохраняем сообщение в БД
|
||
async with AsyncSessionLocal() as session:
|
||
msg_repo = MessageRepository(session)
|
||
msg = await msg_repo.add_message(
|
||
text=text,
|
||
title=context.user_data['message_title']
|
||
)
|
||
context.user_data['message_id'] = msg.id
|
||
|
||
# Теперь показываем список групп для выбора
|
||
async with AsyncSessionLocal() as session:
|
||
group_repo = GroupRepository(session)
|
||
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
|
||
|
||
# Создаем клавиатуру с группами
|
||
keyboard = []
|
||
for group in groups:
|
||
callback = f"select_group_{group.id}"
|
||
keyboard.append([InlineKeyboardButton(
|
||
f"☐ {group.title} (delay: {group.slow_mode_delay}s)",
|
||
callback_data=callback
|
||
)])
|
||
|
||
keyboard.append([InlineKeyboardButton("✔️ Готово", callback_data="done_groups")])
|
||
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data=CallbackType.MAIN_MENU.value)])
|
||
|
||
text = f"""✅ Сообщение создано: <b>{context.user_data['message_title']}</b>
|
||
|
||
Выберите группы для отправки (нажмите на каждую):"""
|
||
|
||
await message.reply_text(
|
||
text,
|
||
parse_mode='HTML',
|
||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||
)
|
||
|
||
context.user_data['selected_groups'] = set()
|
||
context.user_data['message_state'] = STATE_SELECTING_GROUPS
|
||
|
||
|
||
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', set())
|
||
|
||
if not selected:
|
||
await query.answer("❌ Выберите хотя бы одну группу", show_alert=True)
|
||
return
|
||
|
||
# Добавляем сообщение в выбранные группы
|
||
message_id = context.user_data['message_id']
|
||
|
||
async with AsyncSessionLocal() as session:
|
||
mg_repo = MessageGroupRepository(session)
|
||
for group_id in selected:
|
||
await mg_repo.add_message_to_group(message_id, group_id)
|
||
|
||
text = f"""✅ <b>Сообщение готово!</b>
|
||
|
||
Название: {context.user_data['message_title']}
|
||
Групп выбрано: {len(selected)}
|
||
|
||
Теперь вы можете отправить сообщение нажав кнопку "Отправить" в списке сообщений."""
|
||
|
||
context.user_data['message_state'] = None
|
||
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_main_keyboard())
|
||
return
|
||
|
||
elif callback_data.startswith("select_group_"):
|
||
group_id = int(callback_data.split("_")[2])
|
||
selected = context.user_data.get('selected_groups', set())
|
||
|
||
if group_id in selected:
|
||
selected.discard(group_id)
|
||
else:
|
||
selected.add(group_id)
|
||
|
||
context.user_data['selected_groups'] = selected
|
||
|
||
# Обновляем клавиатуру
|
||
async with AsyncSessionLocal() as session:
|
||
group_repo = GroupRepository(session)
|
||
groups = await group_repo.get_all_active_groups()
|
||
|
||
keyboard = []
|
||
for group in groups:
|
||
callback = f"select_group_{group.id}"
|
||
is_selected = group.id in selected
|
||
prefix = "✅" if is_selected else "☐"
|
||
keyboard.append([InlineKeyboardButton(
|
||
f"{prefix} {group.title} (delay: {group.slow_mode_delay}s)",
|
||
callback_data=callback
|
||
)])
|
||
|
||
keyboard.append([InlineKeyboardButton("✔️ Готово", callback_data="done_groups")])
|
||
keyboard.append([InlineKeyboardButton("❌ Отмена", callback_data=CallbackType.MAIN_MENU.value)])
|
||
|
||
await query.edit_message_text(
|
||
f"Выбрано групп: {len(selected)}",
|
||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||
)
|
||
|
||
await query.answer()
|
||
return SELECT_GROUPS
|
||
|
||
elif callback_data == CallbackType.MAIN_MENU:
|
||
# Отмена
|
||
await query.answer()
|
||
await query.edit_message_text(
|
||
"❌ Создание сообщения отменено",
|
||
reply_markup=get_main_keyboard()
|
||
)
|
||
return ConversationHandler.END
|
||
|
||
await query.answer()
|
||
return SELECT_GROUPS
|