"""Common handlers for the Telegram bot with multi-goal support."""
from __future__ import annotations
import sentry_sdk
import structlog
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import ContextTypes, CommandHandler
from telegram.constants import ParseMode
from core.dependency_injection import get_async_storage
from scheduler.tasks import Scheduler
from utils.subscription import is_subscribed, subscribe_user
from core.metrics import USER_COMMANDS_TOTAL
from utils.helpers import escape_markdown_v2
logger = structlog.get_logger(__name__)
# Text constants - Plain text, will be escaped before sending
WELCOME_TEXT = (
"🎯 Добро пожаловать в Target Assistant Bot!\n\n"
"Я помогу вам:\n"
"• 📝 Ставить и управлять целями\n"
"• 📋 Создавать ежедневные планы\n"
"• 📊 Отслеживать прогресс\n"
"• 💪 Получать мотивацию\n\n"
"Возможности:\n"
"• Поддержка до 10 активных целей\n"
"• Автоматическое планирование задач\n"
"• Умная статистика и аналитика\n"
"• Интеграция с Google Sheets\n\n"
"Используйте /my_goals для начала работы с целями!"
)
HELP_TEXT = (
"🤖 Помощь по Target Assistant Bot\n\n"
"Основные команды:\n"
"• /my_goals - управление целями\n"
"• /add_goal - создать новую цель (через кнопки)\n"
"• /setgoal - создать цель (через диалог)\n"
"• /today - задачи на сегодня\n"
"• /status - общий прогресс\n"
"• /check - отметить выполнение\n"
"• /motivation - получить мотивацию\n"
"• /reset - сброс всех данных\n\n"
"Возможности:\n"
"• Создавайте до 10 активных целей\n"
"• Устанавливайте приоритеты и теги\n"
"• Отслеживайте прогресс в реальном времени\n"
"• Получайте персонализированные планы\n\n"
"Все данные синхронизируются с Google Sheets!"
)
CANCEL_TEXT = "❌ Операция отменена."
UNKNOWN_TEXT = (
"🤔 Неизвестная команда.\n" "Используйте /help для списка доступных команд."
)
RESET_SUCCESS_TEXT = (
"✅ Все ваши данные успешно удалены.\n"
"Используйте /start для повторной настройки."
)
RESET_CONFIRM_TEXT = (
"⚠️ \\*ВНИМАНИЕ\\!\\*\n\n" # Properly escape * and ! for MarkdownV2
"Вы собираетесь удалить \\*все\\* ваши цели и данные\\.\n"
"Это действие \\*нельзя отменить\\*\\!\n\n"
"Вы уверены?"
)
[документация]
def start_handler(scheduler: Scheduler) -> CommandHandler:
"""Create start command handler with scheduler dependency."""
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /start command - welcome user and setup."""
USER_COMMANDS_TOTAL.labels(command_name="/start").inc()
if not update.effective_user or not update.message:
return
user_id = update.effective_user.id
logger.info("User started bot", user_id=user_id)
# Subscribe user and create spreadsheet
subscribe_user(user_id)
storage = get_async_storage()
await storage.create_spreadsheet(user_id)
# Add scheduled jobs for this user
scheduler.add_user_jobs(context.bot, user_id)
# Send welcome message with inline keyboard
keyboard = [
[InlineKeyboardButton("🎯 Мои цели", callback_data="back_to_goals")],
[InlineKeyboardButton("➕ Создать цель", callback_data="add_goal")],
[
InlineKeyboardButton(
"📊 Открыть таблицу", callback_data="show_spreadsheet"
)
],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
escape_markdown_v2(WELCOME_TEXT),
parse_mode=ParseMode.MARKDOWN_V2,
reply_markup=reply_markup,
)
return CommandHandler("start", start_command)
[документация]
async def help_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /help command."""
USER_COMMANDS_TOTAL.labels(command_name="/help").inc()
if not update.effective_user or not update.message:
return
user_id = update.effective_user.id
logger.info("User requested help", user_id=user_id)
await update.message.reply_text(
escape_markdown_v2(HELP_TEXT),
parse_mode=ParseMode.MARKDOWN_V2,
disable_web_page_preview=True,
)
[документация]
async def cancel_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /cancel command."""
USER_COMMANDS_TOTAL.labels(command_name="/cancel").inc()
if not update.effective_user or not update.message:
return
user_id = update.effective_user.id
logger.info("User cancelled operation", user_id=user_id)
await update.message.reply_text(
escape_markdown_v2(CANCEL_TEXT), parse_mode=ParseMode.MARKDOWN_V2
)
[документация]
async def reset_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /reset command - show confirmation dialog."""
USER_COMMANDS_TOTAL.labels(command_name="/reset").inc()
if not update.effective_user or not update.message:
return
user_id = update.effective_user.id
if not await is_subscribed(user_id):
await update.message.reply_text(
escape_markdown_v2(
"❌ Вы не подписаны на бота. Используйте /start для начала."
),
parse_mode=ParseMode.MARKDOWN_V2,
)
return
keyboard = [
[
InlineKeyboardButton("⚠️ Да, удалить все", callback_data="confirm_reset"),
InlineKeyboardButton("❌ Отмена", callback_data="cancel_reset"),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
RESET_CONFIRM_TEXT,
parse_mode=ParseMode.MARKDOWN_V2,
reply_markup=reply_markup,
)
[документация]
async def confirm_reset(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle reset confirmation."""
query = update.callback_query
if not query or not query.from_user:
return
await query.answer()
user_id = query.from_user.id
logger.info("User confirmed reset", user_id=user_id)
try:
storage = get_async_storage()
await storage.delete_spreadsheet(user_id)
await query.edit_message_text(
escape_markdown_v2(RESET_SUCCESS_TEXT),
parse_mode=ParseMode.MARKDOWN_V2,
)
except Exception as e:
logger.error("Error during reset", user_id=user_id, error=str(e))
await query.edit_message_text(
escape_markdown_v2(
"❌ Произошла ошибка при сбросе данных. Попробуйте позже."
),
parse_mode=ParseMode.MARKDOWN_V2,
)
[документация]
async def cancel_reset(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle reset cancellation."""
query = update.callback_query
if not query:
return
await query.answer()
await query.edit_message_text(
escape_markdown_v2("❌ Сброс данных отменен."), parse_mode=ParseMode.MARKDOWN_V2
)
[документация]
async def unknown_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle unknown commands."""
USER_COMMANDS_TOTAL.labels(command_name="/unknown").inc()
if not update.effective_user or not update.message:
return
user_id = update.effective_user.id
logger.info("User sent unknown command", user_id=user_id)
await update.message.reply_text(
escape_markdown_v2(UNKNOWN_TEXT), parse_mode=ParseMode.MARKDOWN_V2
)