"""
Telegram бот для уведомлений и интеграции.
"""
import logging
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton
from telegram.ext import (
Application,
CommandHandler,
CallbackQueryHandler,
MessageHandler,
filters,
ContextTypes,
)
from django.conf import settings
from django.utils import timezone
from asgiref.sync import sync_to_async
logger = logging.getLogger(__name__)
def get_main_keyboard(role=None):
"""
Получить основную клавиатуру в зависимости от роли.
Args:
role: Роль пользователя ('mentor', 'client', 'parent', None)
Returns:
ReplyKeyboardMarkup: Клавиатура с кнопками
"""
if role == 'mentor':
keyboard = [
[KeyboardButton("📅 Расписание"), KeyboardButton("📚 Следующее занятие")],
[KeyboardButton("📝 Домашние задания"), KeyboardButton("👥 Клиенты")],
[KeyboardButton("📊 Статистика"), KeyboardButton("⚙️ Настройки")],
[KeyboardButton("ℹ️ Статус"), KeyboardButton("❓ Помощь")]
]
elif role == 'client':
keyboard = [
[KeyboardButton("📅 Моё расписание"), KeyboardButton("📚 Следующее занятие")],
[KeyboardButton("📝 Мои задания"), KeyboardButton("📊 Мой прогресс")],
[KeyboardButton("⚙️ Настройки"), KeyboardButton("❓ Помощь")]
]
elif role == 'parent':
keyboard = [
[KeyboardButton("📅 Расписание детей"), KeyboardButton("📚 Следующее занятие")],
[KeyboardButton("📝 Задания детей"), KeyboardButton("⚙️ Настройки")],
[KeyboardButton("ℹ️ Статус"), KeyboardButton("❓ Помощь")]
]
else:
# Для не связанных пользователей
keyboard = [
[KeyboardButton("🔗 Связать аккаунт"), KeyboardButton("❓ Помощь")]
]
return ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=False)
async def get_user_keyboard(telegram_id):
"""
Получить клавиатуру для пользователя по его telegram_id.
Args:
telegram_id: ID пользователя в Telegram
Returns:
ReplyKeyboardMarkup: Клавиатура с кнопками
"""
from apps.users.models import User
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
role = db_user.role if db_user else None
return get_main_keyboard(role)
class TelegramBot:
"""Класс для управления Telegram ботом."""
def __init__(self):
"""Инициализация бота."""
self.token = settings.TELEGRAM_BOT_TOKEN
self.application = None
self.use_webhook = getattr(settings, 'TELEGRAM_USE_WEBHOOK', False)
self.webhook_url = getattr(settings, 'TELEGRAM_WEBHOOK_URL', None)
self.webhook_secret_token = getattr(settings, 'TELEGRAM_WEBHOOK_SECRET_TOKEN', None)
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обработчик команды /start.
Приветствие и инструкции по связыванию аккаунта.
"""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
# Проверяем связан ли аккаунт
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
# Если аккаунт не связан, показываем общее приветствие
welcome_message = f"""
👋 Привет, {user.first_name}!
Я бот образовательной платформы. Я буду присылать вам уведомления о:
• Новых занятиях
• Домашних заданиях
• Сообщениях от ментора/ученика
• Напоминаниях о занятиях
📱 Чтобы связать ваш аккаунт:
1. Войдите на платформу
2. Перейдите в Профиль → Настройки → Telegram
3. Нажмите "Связать Telegram"
4. Введите код связывания
Или используйте команду:
/link <ваш_код_связывания>
После связывания вы увидите команды, доступные для вашей роли.
"""
else:
# Если аккаунт связан, показываем приветствие в зависимости от роли
role = db_user.role
user_name = db_user.get_full_name() or db_user.email
if role == 'mentor':
welcome_message = f"""
👋 Привет, {user_name}!
Я бот образовательной платформы для менторов.
Я буду присылать вам уведомления о:
• Новых запросах на занятия
• Отменах занятий
• Сданных домашних заданиях
• Сообщениях от учеников
• Напоминаниях о занятиях
📚 Доступные команды:
/help - Полный список команд
/schedule - Расписание занятий
/nextlesson - Следующее занятие
/homework - Домашние задания на проверку
/clients - Список клиентов
/stats - Статистика
/settings - Настройки уведомлений
/status - Статус аккаунта
"""
keyboard = get_main_keyboard('mentor')
await update.message.reply_text(
welcome_message,
reply_markup=keyboard
)
return
elif role == 'client':
welcome_message = f"""
👋 Привет, {user_name}!
Я бот образовательной платформы для учеников.
Я буду присылать вам уведомления о:
• Новых занятиях
• Отменах занятий
• Новых домашних заданиях
• Проверенных домашних заданиях
• Сообщениях от ментора
• Напоминаниях о занятиях
📚 Доступные команды:
/help - Полный список команд
/schedule - Моё расписание
/nextlesson - Следующее занятие
/homework - Мои домашние задания
/progress - Мой прогресс обучения
/settings - Настройки уведомлений
/status - Статус аккаунта
"""
keyboard = get_main_keyboard('client')
await update.message.reply_text(
welcome_message,
reply_markup=keyboard
)
return
elif role == 'parent':
welcome_message = f"""
👋 Привет, {user_name}!
Я бот образовательной платформы для родителей.
Я буду присылать вам уведомления о:
• Занятиях ваших детей
• Домашних заданиях детей
• Прогрессе обучения
• Отчётах от менторов
📚 Доступные команды:
/help - Полный список команд
/schedule - Расписание детей
/nextlesson - Следующее занятие ребёнка
/homework - Домашние задания детей
/settings - Настройки уведомлений
/status - Статус аккаунта
"""
keyboard = get_main_keyboard('parent')
await update.message.reply_text(
welcome_message,
reply_markup=keyboard
)
return
else:
# Для других ролей (admin и т.д.)
welcome_message = f"""
👋 Привет, {user_name}!
Я бот образовательной платформы.
Я буду присылать вам уведомления о событиях на платформе.
📚 Доступные команды:
/help - Полный список команд
/settings - Настройки уведомлений
/status - Статус аккаунта
"""
keyboard = get_main_keyboard(None)
await update.message.reply_text(
welcome_message,
reply_markup=keyboard
)
return
# Если дошли сюда, отправляем без клавиатуры
await update.message.reply_text(welcome_message)
async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /help."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
# Проверяем связан ли аккаунт
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
# Если аккаунт не связан, показываем общую справку
help_text = """
🔔 Доступные команды:
/start - Начать работу с ботом
/link <код> - Связать аккаунт с платформой
/help - Эта справка
💡 Как связать аккаунт:
1. Получите код связывания на платформе (Профиль → Telegram)
2. Отправьте команду: /link ВАШ_КОД
После связывания вы увидите команды, доступные для вашей роли.
"""
else:
# Если аккаунт связан, показываем справку в зависимости от роли
role = db_user.role
if role == 'mentor':
help_text = """
👨🏫 Справка для менторов:
🔔 Основные команды:
/start - Главное меню
/help - Эта справка
/status - Статус аккаунта
/settings - Настройки уведомлений
📚 Расписание:
/schedule - Ближайшие занятия (до 5 занятий)
/nextlesson - Следующее занятие
📝 Домашние задания:
/homework - Задания, требующие проверки
👥 Клиенты:
/clients - Список ваших клиентов со статистикой
/stats - Ваша статистика (занятия, ДЗ, клиенты)
🔗 Управление аккаунтом:
/unlink - Отвязать Telegram аккаунт
💡 Вы будете получать уведомления о:
• Новых запросах на занятия
• Отменах занятий
• Сданных домашних заданиях
• Сообщениях от учеников
"""
elif role == 'client':
help_text = """
👨🎓 Справка для учеников:
🔔 Основные команды:
/start - Главное меню
/help - Эта справка
/status - Статус аккаунта
/settings - Настройки уведомлений
📚 Расписание:
/schedule - Моё расписание (до 5 ближайших занятий)
/nextlesson - Следующее занятие
📝 Домашние задания:
/homework - Мои активные домашние задания
📊 Прогресс:
/progress - Мой прогресс обучения
🔗 Управление аккаунтом:
/unlink - Отвязать Telegram аккаунт
💡 Вы будете получать уведомления о:
• Новых занятиях
• Отменах занятий
• Новых домашних заданиях
• Проверенных домашних заданиях
• Сообщениях от ментора
"""
elif role == 'parent':
help_text = """
👨👩👧 Справка для родителей:
🔔 Основные команды:
/start - Главное меню
/help - Эта справка
/status - Статус аккаунта
/settings - Настройки уведомлений
📚 Расписание детей:
/schedule - Расписание всех детей (до 5 ближайших занятий)
/nextlesson - Следующее занятие ребёнка
📝 Домашние задания:
/homework - Домашние задания детей
🔗 Управление аккаунтом:
/unlink - Отвязать Telegram аккаунт
💡 Вы будете получать уведомления о:
• Занятиях ваших детей
• Домашних заданиях детей
• Прогрессе обучения
• Отчётах от менторов
"""
else:
# Для других ролей (admin и т.д.)
help_text = """
🔔 Доступные команды:
/start - Начать работу с ботом
/help - Эта справка
/settings - Настройки уведомлений
/status - Статус связывания
/unlink - Отвязать аккаунт
💡 Вы будете получать уведомления о событиях на платформе.
"""
# Добавляем клавиатуру если аккаунт связан
if db_user:
keyboard = get_main_keyboard(db_user.role)
await update.message.reply_text(help_text, reply_markup=keyboard)
else:
await update.message.reply_text(help_text)
async def link_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обработчик команды /link.
Связывание Telegram аккаунта с аккаунтом на платформе.
"""
user = update.effective_user
telegram_id = user.id
telegram_username = user.username or ''
# Проверяем наличие кода
if not context.args:
await update.message.reply_text(
"❌ Укажите код связывания.\n\n"
"Использование: /link <код>\n\n"
"Получите код на платформе: Профиль → Настройки → Telegram"
)
return
link_code = context.args[0]
# Проверяем код и связываем аккаунт
from .services import TelegramLinkService
try:
result = await sync_to_async(TelegramLinkService.link_account)(
link_code=link_code,
telegram_id=telegram_id,
telegram_username=telegram_username
)
if result['success']:
user_name = result.get('user_name', 'Пользователь')
# Получаем роль пользователя для клавиатуры
from apps.users.models import User
linked_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
keyboard = get_main_keyboard(linked_user.role if linked_user else None)
await update.message.reply_text(
f"✅ Аккаунт успешно связан!\n\n"
f"👤 {user_name}\n\n"
f"Теперь вы будете получать уведомления в Telegram.",
reply_markup=keyboard
)
else:
keyboard = get_main_keyboard(None)
await update.message.reply_text(
f"❌ Ошибка связывания: {result.get('error', 'Неизвестная ошибка')}",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error linking Telegram account: {e}")
await update.message.reply_text(
"❌ Произошла ошибка при связывании аккаунта.\n"
"Попробуйте позже или обратитесь в поддержку."
)
async def unlink_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Обработчик команды /unlink.
Отвязка Telegram аккаунта.
"""
user = update.effective_user
telegram_id = user.id
from .services import TelegramLinkService
try:
result = await sync_to_async(TelegramLinkService.unlink_account)(telegram_id)
if result['success']:
await update.message.reply_text(
"✅ Аккаунт успешно отвязан.\n\n"
"Вы больше не будете получать уведомления в Telegram.\n\n"
"Чтобы снова связать аккаунт, используйте /link"
)
else:
await update.message.reply_text(
f"❌ {result.get('error', 'Аккаунт не найден')}"
)
except Exception as e:
logger.error(f"Error unlinking Telegram account: {e}")
await update.message.reply_text(
"❌ Произошла ошибка при отвязке аккаунта."
)
async def status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /status."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if db_user:
role_display = db_user.get_role_display()
role_emoji = {
'mentor': '👨🏫',
'client': '👨🎓',
'parent': '👨👩👧',
'admin': '👤'
}.get(db_user.role, '👤')
status_message = (
f"✅ Аккаунт связан\n\n"
f"{role_emoji} {db_user.get_full_name() or db_user.email}\n"
f"📧 {db_user.email}\n"
f"🎭 Роль: {role_display}\n\n"
)
# Дополнительная информация в зависимости от роли
if db_user.role == 'parent':
from apps.users.models import Parent
try:
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
children = await sync_to_async(list)(parent_profile.children.all())
children_count = len(children)
status_message += f"👶 Привязано детей: {children_count}\n\n"
except:
pass
# Проверяем настройки уведомлений
try:
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
notifications_status = "✅ Включены" if preferences.telegram_enabled else "❌ Выключены"
status_message += f"🔔 Уведомления: {notifications_status}"
except:
status_message += "🔔 Уведомления: ⚙️ Настройте на платформе"
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(status_message, parse_mode='HTML', reply_markup=keyboard)
else:
keyboard = get_main_keyboard(None)
await update.message.reply_text(
"❌ Аккаунт не связан\n\n"
"Используйте /link <код> для связывания аккаунта.",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error checking status: {e}")
await update.message.reply_text(
"❌ Ошибка проверки статуса."
)
async def settings_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /settings."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
keyboard = get_main_keyboard(None)
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания.",
reply_markup=keyboard
)
return
# Получаем настройки уведомлений
try:
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
except:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"⚙️ Настройки уведомлений не найдены.\n\n"
"Настройте уведомления на платформе: Профиль → Настройки",
reply_markup=keyboard
)
return
# Формируем расширенное меню настроек
message_text = "⚙️ Настройки\n\n"
# Статус уведомлений
all_status = "✅ Включены" if preferences.enabled else "❌ Выключены"
telegram_status = "✅ Вкл" if preferences.telegram_enabled else "❌ Выкл"
email_status = "✅ Вкл" if preferences.email_enabled else "❌ Выкл"
in_app_status = "✅ Вкл" if preferences.in_app_enabled else "❌ Выкл"
message_text += f"🔔 Все уведомления: {all_status}\n"
message_text += f"📱 Telegram: {telegram_status}\n"
message_text += f"📧 Email: {email_status}\n"
message_text += f"💬 В приложении: {in_app_status}\n\n"
# Режим тишины
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
message_text += f"🔇 Режим тишины: {quiet_start} - {quiet_end}\n\n"
else:
message_text += "🔇 Режим тишины: ❌ Выключен\n\n"
# Часовой пояс
user_tz = db_user.timezone or 'Europe/Moscow'
message_text += f"🕐 Часовой пояс: {user_tz}\n"
# Язык
user_lang = db_user.language or 'ru'
lang_display = 'Русский' if user_lang == 'ru' else 'English'
message_text += f"🌐 Язык: {lang_display}\n"
# Создаем inline клавиатуру
keyboard = []
# Основные настройки уведомлений
keyboard.append([
InlineKeyboardButton(
"🔔 " + ("Выключить все" if preferences.enabled else "Включить все"),
callback_data="settings_toggle_all"
)
])
keyboard.append([
InlineKeyboardButton(
"📱 Telegram: " + ("Выкл" if preferences.telegram_enabled else "Вкл"),
callback_data="settings_toggle_telegram"
),
InlineKeyboardButton(
"📧 Email: " + ("Выкл" if preferences.email_enabled else "Вкл"),
callback_data="settings_toggle_email"
)
])
keyboard.append([
InlineKeyboardButton(
"🔇 Режим тишины",
callback_data="settings_quiet_hours"
),
InlineKeyboardButton(
"📋 Типы уведомлений",
callback_data="settings_notification_types"
)
])
keyboard.append([
InlineKeyboardButton(
"🕐 Часовой пояс",
callback_data="settings_timezone"
),
InlineKeyboardButton(
"🌐 Язык",
callback_data="settings_language"
)
])
# Добавляем кнопку с URL только если это не localhost
frontend_url = settings.FRONTEND_URL
if frontend_url and 'localhost' not in frontend_url and '127.0.0.1' not in frontend_url:
keyboard.append([
InlineKeyboardButton("🌐 Открыть на сайте", url=f"{frontend_url}/profile")
])
reply_markup = InlineKeyboardMarkup(keyboard)
# Добавляем основную клавиатуру вместе с inline кнопками
main_keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
message_text,
parse_mode='HTML',
reply_markup=reply_markup
)
# Отправляем отдельное сообщение с основной клавиатурой
await update.message.reply_text(
"Используйте кнопки ниже для навигации:",
reply_markup=main_keyboard
)
except Exception as e:
logger.error(f"Error getting settings: {e}")
await update.message.reply_text(
"❌ Ошибка получения настроек."
)
async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик нажатий на кнопки."""
query = update.callback_query
await query.answer()
# Сначала обрабатываем настройки (они имеют приоритет)
if query.data.startswith('settings_') or query.data.startswith('toggle_type_') or query.data.startswith('set_timezone_') or query.data.startswith('set_language_'):
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
await query.edit_message_text("❌ Аккаунт не связан.")
return
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
# Переключение всех уведомлений
if query.data == "settings_toggle_all":
preferences.enabled = not preferences.enabled
await sync_to_async(preferences.save)()
await self._refresh_settings_message(query, db_user, preferences)
return
# Переключение Telegram уведомлений
elif query.data == "settings_toggle_telegram":
preferences.telegram_enabled = not preferences.telegram_enabled
await sync_to_async(preferences.save)()
await self._refresh_settings_message(query, db_user, preferences)
return
# Переключение Email уведомлений
elif query.data == "settings_toggle_email":
preferences.email_enabled = not preferences.email_enabled
await sync_to_async(preferences.save)()
await self._refresh_settings_message(query, db_user, preferences)
return
# Режим тишины
elif query.data == "settings_quiet_hours":
await self._handle_quiet_hours(query, db_user, preferences)
return
# Обработка режима тишины
elif query.data.startswith("quiet_hours_"):
from datetime import time
if query.data == "quiet_hours_disable":
preferences.quiet_hours_enabled = False
await sync_to_async(preferences.save)()
await query.answer("Режим тишины выключен")
await self._handle_quiet_hours(query, db_user, preferences)
return
elif query.data == "quiet_hours_enable_22_8":
preferences.quiet_hours_enabled = True
preferences.quiet_hours_start = time(22, 0)
preferences.quiet_hours_end = time(8, 0)
await sync_to_async(preferences.save)()
await query.answer("Режим тишины: 22:00 - 08:00")
await self._refresh_settings_message(query, db_user, preferences)
return
elif query.data == "quiet_hours_enable_23_7":
preferences.quiet_hours_enabled = True
preferences.quiet_hours_start = time(23, 0)
preferences.quiet_hours_end = time(7, 0)
await sync_to_async(preferences.save)()
await query.answer("Режим тишины: 23:00 - 07:00")
await self._refresh_settings_message(query, db_user, preferences)
return
elif query.data == "quiet_hours_custom":
await query.answer("Настройка времени через сайт", show_alert=True)
await self._handle_quiet_hours(query, db_user, preferences)
return
# Типы уведомлений
elif query.data == "settings_notification_types":
await self._handle_notification_types(query, db_user, preferences)
return
# Часовой пояс
elif query.data == "settings_timezone":
await self._handle_timezone(query, db_user)
return
# Язык
elif query.data == "settings_language":
await self._handle_language(query, db_user)
return
# Обработка типов уведомлений
elif query.data.startswith("toggle_type_"):
ntype = query.data.replace("toggle_type_", "")
type_prefs = preferences.type_preferences.get(ntype, {})
if not isinstance(type_prefs, dict):
type_prefs = {}
current = type_prefs.get('telegram', True)
type_prefs['telegram'] = not current
preferences.type_preferences[ntype] = type_prefs
await sync_to_async(preferences.save)()
# Получаем название типа для ответа
from .models import Notification
type_display = dict(Notification.TYPE_CHOICES).get(ntype, ntype)
status = "включены" if not current else "выключены"
await query.answer(f"{type_display} {status}")
await self._handle_notification_types(query, db_user, preferences)
return
# Игнорируем заголовки (noop)
elif query.data == "noop":
await query.answer()
return
# Установка часового пояса
elif query.data.startswith("set_timezone_"):
timezone = query.data.replace("set_timezone_", "")
db_user.timezone = timezone
await sync_to_async(db_user.save)(update_fields=['timezone'])
await query.answer(f"Часовой пояс установлен: {timezone}")
await self._handle_timezone(query, db_user)
return
# Установка языка
elif query.data.startswith("set_language_"):
language = query.data.replace("set_language_", "")
db_user.language = language
await sync_to_async(db_user.save)(update_fields=['language'])
await query.answer(f"Язык установлен: {language}")
await self._handle_language(query, db_user)
return
# Возврат к настройкам
elif query.data == "settings_back":
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
await self._refresh_settings_message(query, db_user, preferences)
return
except Exception as e:
logger.error(f"Error handling settings callback: {e}", exc_info=True)
await query.edit_message_text("❌ Ошибка обновления настроек.")
return
# Обработка домашних заданий
if query.data.startswith('homework_'):
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
await query.answer("❌ Аккаунт не связан", show_alert=True)
return
try:
if query.data.startswith('homework_upload_'):
# Загрузка решения ДЗ
homework_id = int(query.data.replace('homework_upload_', ''))
from apps.homework.models import Homework
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
# Проверяем доступ
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
await query.answer("❌ У вас нет доступа к этому заданию", show_alert=True)
return
# Сохраняем ID задания в контексте
context.user_data['waiting_for_homework_file'] = homework_id
await query.answer("📎 Отправьте файл или фото для решения")
await query.edit_message_text(
f"📎 Загрузка решения\n\n"
f"📝 Задание: {homework.title}\n\n"
f"Отправьте:\n"
f"• Текст решения\n"
f"• Файл (документ)\n"
f"• Фото с решением\n\n"
f"Или отправьте 'отмена' для отмены.",
parse_mode='HTML'
)
return
elif query.data.startswith('homework_detail_'):
# Детали задания
homework_id = int(query.data.replace('homework_detail_', ''))
from apps.homework.models import Homework, HomeworkSubmission
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
deadline_str = homework.deadline.strftime('%d.%m.%Y в %H:%M') if homework.deadline else 'Без дедлайна'
submission = await sync_to_async(
HomeworkSubmission.objects.filter(
homework=homework,
student=db_user
).first
)()
message = f"📝 {homework.title}\n\n"
message += f"📅 Дедлайн: {deadline_str}\n"
if homework.description:
desc = homework.description[:200] + "..." if len(homework.description) > 200 else homework.description
message += f"\n📄 {desc}\n"
if submission:
message += f"\n✅ Статус: Сдано\n"
if submission.status == 'graded' and submission.score is not None:
message += f"🎯 Оценка: {submission.score}/{homework.max_score}\n"
elif submission.status == 'returned':
message += f"🔄 Возвращено на доработку\n"
else:
message += f"⏳ Ожидает проверки\n"
else:
message += f"\n⏳ Статус: Не сдано\n"
inline_keyboard = []
if not submission or submission.status == 'returned':
inline_keyboard.append([
InlineKeyboardButton(
"📎 Загрузить решение",
callback_data=f"homework_upload_{homework.id}"
)
])
if submission:
inline_keyboard.append([
InlineKeyboardButton(
"👁️ Просмотреть решение",
callback_data=f"homework_view_{submission.id}"
)
])
inline_keyboard.append([
InlineKeyboardButton(
"◀️ Назад к списку",
callback_data="homework_back"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard)
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
return
elif query.data.startswith('homework_view_'):
# Просмотр решения
submission_id = int(query.data.replace('homework_view_', ''))
from apps.homework.models import HomeworkSubmission
submission = await sync_to_async(HomeworkSubmission.objects.select_related('homework').get)(id=submission_id)
if submission.student != db_user and db_user.role != 'mentor':
await query.answer("❌ У вас нет доступа", show_alert=True)
return
message = f"👁️ Решение ДЗ\n\n"
message += f"📝 Задание: {submission.homework.title}\n"
message += f"👤 Студент: {submission.student.get_full_name()}\n"
message += f"📅 Сдано: {submission.submitted_at.strftime('%d.%m.%Y в %H:%M') if submission.submitted_at else 'Неизвестно'}\n"
message += f"📊 Статус: {submission.get_status_display()}\n"
if submission.score is not None:
message += f"🎯 Оценка: {submission.score}/{submission.homework.max_score}\n"
if submission.feedback:
feedback = submission.feedback[:200] + "..." if len(submission.feedback) > 200 else submission.feedback
message += f"\n💬 Отзыв: {feedback}\n"
if submission.attachment:
message += f"\n📎 Файл прикреплен"
inline_keyboard = [[
InlineKeyboardButton(
"🌐 Открыть на сайте",
url=f"{settings.FRONTEND_URL}/homework"
)
]]
reply_markup = InlineKeyboardMarkup(inline_keyboard)
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
return
elif query.data == 'homework_back':
# Возврат к списку заданий
# Отправляем новое сообщение со списком
from apps.homework.models import Homework, HomeworkSubmission
from django.utils import timezone
homeworks = await sync_to_async(list)(
Homework.objects.filter(
assigned_to=db_user,
deadline__gte=timezone.now()
).order_by('deadline')[:5]
)
if not homeworks:
await query.edit_message_text("📝 У вас нет активных домашних заданий.")
return
message = "📝 Активные домашние задания:\n\n"
inline_keyboard = []
for hw in homeworks:
deadline_str = hw.deadline.strftime('%d.%m.%Y') if hw.deadline else 'Без дедлайна'
submission = await sync_to_async(
HomeworkSubmission.objects.filter(
homework=hw,
student=db_user
).first
)()
status = "✅ Сдано" if submission else "⏳ Не сдано"
message += f"{hw.title}\n"
message += f"📅 Дедлайн: {deadline_str}\n"
message += f"{status}\n\n"
inline_keyboard.append([
InlineKeyboardButton(
f"📝 {hw.title[:30]}",
callback_data=f"homework_detail_{hw.id}"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard)
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
return
except Exception as e:
logger.error(f"Error handling homework callback: {e}", exc_info=True)
await query.answer("❌ Ошибка обработки запроса", show_alert=True)
return
# Обработка деталей занятия
if query.data.startswith('lesson_detail_'):
try:
lesson_id = int(query.data.replace('lesson_detail_', ''))
from apps.schedule.models import Lesson
from django.utils import timezone
import pytz
lesson = await sync_to_async(
Lesson.objects.select_related('mentor', 'client', 'client__user', 'subject').get
)(id=lesson_id)
user = update.effective_user
telegram_id = user.id
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
await query.answer("❌ Аккаунт не связан", show_alert=True)
return
# Проверяем доступ
has_access = False
if db_user.role == 'mentor' and lesson.mentor == db_user:
has_access = True
elif db_user.role == 'client' and lesson.client and lesson.client.user == db_user:
has_access = True
elif db_user.role == 'parent' and lesson.client and lesson.client.user.parent_profile and lesson.client.user.parent_profile.user == db_user:
has_access = True
if not has_access:
await query.answer("❌ У вас нет доступа к этому занятию", show_alert=True)
return
# Формируем сообщение
from apps.users.utils import get_user_timezone
user_tz = get_user_timezone(db_user.timezone or 'UTC')
if timezone.is_aware(lesson.start_time):
local_start = lesson.start_time.astimezone(user_tz)
local_end = lesson.end_time.astimezone(user_tz) if lesson.end_time else None
else:
utc_start = timezone.make_aware(lesson.start_time, pytz.UTC)
local_start = utc_start.astimezone(user_tz)
local_end = lesson.end_time.astimezone(user_tz) if lesson.end_time and timezone.is_aware(lesson.end_time) else None
message = f"📚 {lesson.title}\n\n"
if lesson.subject:
message += f"📖 Предмет: {lesson.subject.name}\n"
message += f"🕐 Начало: {local_start.strftime('%d.%m.%Y в %H:%M')}\n"
if local_end:
message += f"🕐 Окончание: {local_end.strftime('%d.%m.%Y в %H:%M')}\n"
message += f"⏱ Длительность: {lesson.duration} минут\n"
message += f"📊 Статус: {lesson.get_status_display()}\n\n"
if db_user.role == 'mentor':
if lesson.client:
message += f"👤 Студент: {lesson.client.user.get_full_name()}\n"
else:
message += f"👨🏫 Ментор: {lesson.mentor.get_full_name()}\n"
if lesson.description:
desc = lesson.description[:200] + "..." if len(lesson.description) > 200 else lesson.description
message += f"\n📄 {desc}\n"
if lesson.meeting_url:
message += f"\n🔗 Ссылка на видеоконференцию\n"
if lesson.mentor_grade is not None:
message += f"\n🎯 Оценка: {lesson.mentor_grade}/100\n"
inline_keyboard = [[
InlineKeyboardButton(
"🌐 Открыть на сайте",
url=f"{settings.FRONTEND_URL}/schedule"
)
]]
reply_markup = InlineKeyboardMarkup(inline_keyboard)
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
except Lesson.DoesNotExist:
await query.answer("❌ Занятие не найдено", show_alert=True)
except Exception as e:
logger.error(f"Error handling lesson detail: {e}", exc_info=True)
await query.answer("❌ Ошибка обработки запроса", show_alert=True)
return
# Обработка деталей клиента (для менторов)
if query.data.startswith('client_detail_'):
try:
client_id = int(query.data.replace('client_detail_', ''))
from apps.users.models import Client, User
from apps.schedule.models import Lesson
from apps.homework.models import HomeworkSubmission
from django.utils import timezone
from datetime import timedelta
client = await sync_to_async(
Client.objects.select_related('user').get
)(id=client_id)
user = update.effective_user
telegram_id = user.id
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user or db_user.role != 'mentor' or db_user not in await sync_to_async(list)(client.mentors.all()):
await query.answer("❌ У вас нет доступа", show_alert=True)
return
client_name = client.user.get_full_name() or client.user.email
message = f"👤 {client_name}\n\n"
# Статистика занятий
all_lessons = await sync_to_async(list)(
Lesson.objects.filter(mentor=db_user, client=client)
)
total_lessons = len(all_lessons)
completed_lessons = len([l for l in all_lessons if l.status == 'completed'])
upcoming_lessons = len([
l for l in all_lessons
if l.start_time >= timezone.now() and l.status == 'scheduled'
])
# Занятия за месяц
month_ago = timezone.now() - timedelta(days=30)
lessons_this_month = len([
l for l in all_lessons
if l.start_time >= month_ago
])
message += f"📚 Занятия:\n"
message += f"• Всего: {total_lessons}\n"
message += f"• Завершено: {completed_lessons}\n"
message += f"• Предстоящих: {upcoming_lessons}\n"
message += f"• За месяц: {lessons_this_month}\n\n"
# Статистика ДЗ
all_submissions = await sync_to_async(list)(
HomeworkSubmission.objects.filter(
homework__mentor=db_user,
student=client.user
)
)
total_homeworks = len(all_submissions)
pending_homeworks = len([s for s in all_submissions if s.status == 'pending'])
graded_homeworks = len([s for s in all_submissions if s.status == 'graded'])
message += f"📝 Домашние задания:\n"
message += f"• Всего решений: {total_homeworks}\n"
message += f"• На проверке: {pending_homeworks}\n"
message += f"• Проверено: {graded_homeworks}\n"
if graded_homeworks > 0:
scores = [s.score for s in all_submissions if s.score is not None]
avg_score = sum(scores) / len(scores) if scores else 0
message += f"• Средний балл: {avg_score:.1f}\n"
# Доходы от клиента
lessons_with_price = [l for l in all_lessons if l.price and l.status == 'completed']
if lessons_with_price:
total_revenue = sum([float(l.price) for l in lessons_with_price])
message += f"\n💰 Доходы:\n"
message += f"• Всего: {total_revenue:.2f} ₽\n"
inline_keyboard = [[
InlineKeyboardButton(
"🌐 Открыть на сайте",
url=f"{settings.FRONTEND_URL}/students"
),
InlineKeyboardButton(
"◀️ Назад к списку",
callback_data="clients_back"
)
]]
reply_markup = InlineKeyboardMarkup(inline_keyboard)
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
except Client.DoesNotExist:
await query.answer("❌ Клиент не найден", show_alert=True)
except Exception as e:
logger.error(f"Error handling client detail: {e}", exc_info=True)
await query.answer("❌ Ошибка обработки запроса", show_alert=True)
return
# Обработка возврата к списку клиентов
if query.data == 'clients_back':
try:
user = update.effective_user
telegram_id = user.id
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user or db_user.role != 'mentor':
await query.answer("❌ Ошибка", show_alert=True)
return
from apps.users.models import Client
clients = await sync_to_async(list)(
Client.objects.filter(mentors=db_user).select_related('user')[:10]
)
if not clients:
await query.edit_message_text("👥 У вас пока нет клиентов.")
return
message = "👥 Ваши клиенты:\n\n"
inline_keyboard = []
for client in clients:
client_name = client.user.get_full_name() or client.user.email
message += f"👤 {client_name}\n\n"
inline_keyboard.append([
InlineKeyboardButton(
f"👤 {client_name[:30]}",
callback_data=f"client_detail_{client.id}"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard)
await query.edit_message_text(message, parse_mode='HTML', reply_markup=reply_markup)
except Exception as e:
logger.error(f"Error handling clients back: {e}", exc_info=True)
await query.answer("❌ Ошибка", show_alert=True)
return
# Обработка подтверждения присутствия
if query.data.startswith('attendance_yes_') or query.data.startswith('attendance_no_'):
try:
lesson_id = int(query.data.split('_')[-1])
response_bool = query.data.startswith('attendance_yes_')
from apps.schedule.models import Lesson
from django.utils import timezone
lesson = await sync_to_async(Lesson.objects.select_related('client', 'client__user', 'mentor').get)(id=lesson_id)
user = update.effective_user
telegram_id = user.id
# Проверяем, что пользователь - студент этого занятия
if not lesson.client or lesson.client.user.telegram_id != telegram_id:
await query.edit_message_text(
"❌ Ошибка: вы не являетесь студентом этого занятия"
)
return
# Сохраняем ответ
lesson.attendance_confirmed = response_bool
lesson.attendance_response_at = timezone.now()
await sync_to_async(lesson.save)(update_fields=['attendance_confirmed', 'attendance_response_at'])
# Отправляем уведомление ментору
from apps.notifications.services import NotificationService
await sync_to_async(NotificationService.send_attendance_response_to_mentor)(lesson, response_bool)
response_text = "будете присутствовать" if response_bool else "не сможете присутствовать"
await query.edit_message_text(
f"✅ Ответ сохранен\n\n"
f"Вы подтвердили, что {response_text} на занятии:\n"
f"{lesson.title}\n\n"
f"Преподаватель получил уведомление."
)
except Lesson.DoesNotExist:
await query.edit_message_text("❌ Занятие не найдено")
except Exception as e:
logger.error(f"Error processing attendance confirmation: {e}")
await query.edit_message_text("❌ Ошибка обработки ответа")
return
# Обработка настроек уведомлений
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
await query.edit_message_text("❌ Аккаунт не связан.")
return
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
# Переключение всех уведомлений
if query.data == "settings_toggle_all":
preferences.enabled = not preferences.enabled
await sync_to_async(preferences.save)()
await self._refresh_settings_message(query, db_user, preferences)
return
# Переключение Telegram уведомлений
elif query.data == "settings_toggle_telegram":
preferences.telegram_enabled = not preferences.telegram_enabled
await sync_to_async(preferences.save)()
await self._refresh_settings_message(query, db_user, preferences)
return
# Переключение Email уведомлений
elif query.data == "settings_toggle_email":
preferences.email_enabled = not preferences.email_enabled
await sync_to_async(preferences.save)()
await self._refresh_settings_message(query, db_user, preferences)
return
# Режим тишины
elif query.data == "settings_quiet_hours":
await self._handle_quiet_hours(query, db_user, preferences)
return
# Типы уведомлений
elif query.data == "settings_notification_types":
await self._handle_notification_types(query, db_user, preferences)
return
# Часовой пояс
elif query.data == "settings_timezone":
await self._handle_timezone(query, db_user)
return
# Язык
elif query.data == "settings_language":
await self._handle_language(query, db_user)
return
# Обработка типов уведомлений
elif query.data.startswith("toggle_type_"):
ntype = query.data.replace("toggle_type_", "")
type_prefs = preferences.type_preferences.get(ntype, {})
if not isinstance(type_prefs, dict):
type_prefs = {}
current = type_prefs.get('telegram', True)
type_prefs['telegram'] = not current
preferences.type_preferences[ntype] = type_prefs
await sync_to_async(preferences.save)()
await query.answer("Настройка обновлена")
await self._handle_notification_types(query, db_user, preferences)
return
# Установка часового пояса
elif query.data.startswith("set_timezone_"):
timezone = query.data.replace("set_timezone_", "")
db_user.timezone = timezone
await sync_to_async(db_user.save)(update_fields=['timezone'])
await query.answer(f"Часовой пояс установлен: {timezone}")
await self._handle_timezone(query, db_user)
return
# Установка языка
elif query.data.startswith("set_language_"):
language = query.data.replace("set_language_", "")
db_user.language = language
await sync_to_async(db_user.save)(update_fields=['language'])
await query.answer(f"Язык установлен: {language}")
await self._handle_language(query, db_user)
return
# Возврат к настройкам
elif query.data == "settings_back":
preferences = await sync_to_async(lambda: db_user.notification_preferences)()
await self._refresh_settings_message(query, db_user, preferences)
return
# Старый обработчик для обратной совместимости
elif query.data == "toggle_notifications":
preferences.telegram_enabled = not preferences.telegram_enabled
await sync_to_async(preferences.save)()
await self._refresh_settings_message(query, db_user, preferences)
return
except Exception as e:
logger.error(f"Error handling settings callback: {e}", exc_info=True)
await query.edit_message_text(
"❌ Ошибка обновления настроек."
)
async def schedule_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /schedule - показать расписание."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания аккаунта."
)
return
# Получаем ближайшие занятия
from apps.schedule.models import Lesson
from django.utils import timezone
now = timezone.now()
# Для ментора - все занятия
if db_user.role == 'mentor':
lessons = await sync_to_async(list)(
Lesson.objects.filter(
mentor=db_user,
start_time__gte=now
).select_related('mentor', 'client', 'client__user').order_by('start_time')[:5]
)
# Для клиента - только его занятия
elif db_user.role == 'client':
# Проверяем наличие Client профиля
try:
from apps.users.models import Client
client_profile = await sync_to_async(
Client.objects.filter(user=db_user).first
)()
if not client_profile:
await update.message.reply_text(
"❌ Профиль клиента не найден.\n\n"
"Обратитесь к администратору для настройки профиля."
)
return
except Exception as e:
logger.error(f"Error getting client profile: {e}")
await update.message.reply_text(
"❌ Ошибка получения профиля клиента."
)
return
lessons = await sync_to_async(list)(
Lesson.objects.filter(
client=client_profile,
start_time__gte=now
).select_related('mentor', 'client', 'client__user').order_by('start_time')[:5]
)
# Для родителя - занятия всех детей
elif db_user.role == 'parent':
from apps.users.models import Parent
try:
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
children = await sync_to_async(list)(parent_profile.children.all())
if not children:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ У вас нет привязанных детей.\n\n"
"Обратитесь к администратору для привязки детей к вашему аккаунту.",
reply_markup=keyboard
)
return
# Получаем занятия всех детей
child_clients = list(children) # children уже список Client объектов
lessons = await sync_to_async(list)(
Lesson.objects.filter(
client__in=child_clients,
start_time__gte=now
).select_related('mentor', 'client', 'client__user').order_by('start_time')[:5]
)
except:
await update.message.reply_text(
"❌ Ошибка получения данных родителя."
)
return
else:
await update.message.reply_text(
"❌ Эта команда доступна только для менторов, клиентов и родителей."
)
return
if not lessons:
await update.message.reply_text(
"📅 У вас нет предстоящих занятий."
)
return
message = "📅 Ближайшие занятия:\n\n"
for lesson in lessons:
import pytz
from apps.users.utils import get_user_timezone
user_tz = get_user_timezone(db_user.timezone or 'UTC')
if timezone.is_aware(lesson.start_time):
local_time = lesson.start_time.astimezone(user_tz)
else:
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
local_time = utc_time.astimezone(user_tz)
time_str = local_time.strftime('%d.%m.%Y в %H:%M')
if db_user.role == 'mentor':
student_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Студент'
message += f"📚 {lesson.title}\n"
message += f"👤 {student_name}\n"
elif db_user.role == 'parent':
child_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Ребёнок'
message += f"📚 {lesson.title}\n"
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
message += f"👶 {child_name}\n"
else:
message += f"📚 {lesson.title}\n"
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
message += f"🕐 {time_str}\n\n"
await update.message.reply_text(message, parse_mode='HTML')
except Exception as e:
logger.error(f"Error in schedule_command: {e}", exc_info=True)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения расписания.\n\n"
"Попробуйте позже или обратитесь в поддержку.",
reply_markup=keyboard
)
async def nextlesson_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /nextlesson - показать следующее занятие."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
keyboard = get_main_keyboard(None)
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания аккаунта.",
reply_markup=keyboard
)
return
from apps.schedule.models import Lesson
from django.utils import timezone
now = timezone.now()
# Находим ближайшее занятие
if db_user.role == 'mentor':
lesson = await sync_to_async(
Lesson.objects.filter(
mentor=db_user,
start_time__gte=now
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
)()
elif db_user.role == 'client':
# Проверяем наличие Client профиля
try:
from apps.users.models import Client
client_profile = await sync_to_async(
Client.objects.filter(user=db_user).first
)()
if not client_profile:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Профиль клиента не найден.\n\n"
"Обратитесь к администратору для настройки профиля.",
reply_markup=keyboard
)
return
except Exception as e:
logger.error(f"Error getting client profile: {e}")
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения профиля клиента.",
reply_markup=keyboard
)
return
lesson = await sync_to_async(
Lesson.objects.filter(
client=client_profile,
start_time__gte=now
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
)()
elif db_user.role == 'parent':
from apps.users.models import Parent
try:
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
children = await sync_to_async(list)(parent_profile.children.all())
if not children:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ У вас нет привязанных детей.",
reply_markup=keyboard
)
return
child_clients = list(children) # children уже список Client объектов
lesson = await sync_to_async(
Lesson.objects.filter(
client__in=child_clients,
start_time__gte=now
).select_related('mentor', 'client', 'client__user').order_by('start_time').first
)()
except:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения данных родителя.",
reply_markup=keyboard
)
return
else:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Эта команда доступна только для менторов, клиентов и родителей.",
reply_markup=keyboard
)
return
if not lesson:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"📅 У вас нет предстоящих занятий.",
reply_markup=keyboard
)
return
import pytz
from apps.users.utils import get_user_timezone
user_tz = get_user_timezone(db_user.timezone or 'UTC')
if timezone.is_aware(lesson.start_time):
local_time = lesson.start_time.astimezone(user_tz)
else:
utc_time = timezone.make_aware(lesson.start_time, pytz.UTC)
local_time = utc_time.astimezone(user_tz)
time_str = local_time.strftime('%d.%m.%Y в %H:%M')
message = "📚 Следующее занятие:\n\n"
message += f"{lesson.title}\n"
if db_user.role == 'mentor':
student_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Студент'
message += f"👤 {student_name}\n"
elif db_user.role == 'parent':
child_name = lesson.client.user.get_full_name() if lesson.client and lesson.client.user else 'Ребёнок'
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
message += f"👶 {child_name}\n"
else:
message += f"👨🏫 {lesson.mentor.get_full_name()}\n"
message += f"🕐 {time_str}\n"
if lesson.description:
message += f"\n📝 {lesson.description[:200]}"
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
logger.error(f"Error in nextlesson_command: {e}", exc_info=True)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения следующего занятия.\n\n"
"Попробуйте позже или обратитесь в поддержку.",
reply_markup=keyboard
)
async def homework_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /homework - показать домашние задания."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания аккаунта."
)
return
from apps.homework.models import Homework, HomeworkSubmission
# Для клиента - показать его задания
if db_user.role == 'client':
homeworks = await sync_to_async(list)(
Homework.objects.filter(
assigned_to=db_user,
deadline__gte=timezone.now()
).order_by('deadline')[:5]
)
if not homeworks:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"📝 У вас нет активных домашних заданий.",
reply_markup=keyboard
)
return
# Если только одно задание, показываем детали с кнопками
if len(homeworks) == 1:
hw = homeworks[0]
deadline_str = hw.deadline.strftime('%d.%m.%Y в %H:%M') if hw.deadline else 'Без дедлайна'
# Проверяем сдано ли
submission = await sync_to_async(
HomeworkSubmission.objects.filter(
homework=hw,
student=db_user
).first
)()
message = f"📝 {hw.title}\n\n"
message += f"📅 Дедлайн: {deadline_str}\n"
if hw.description:
desc = hw.description[:200] + "..." if len(hw.description) > 200 else hw.description
message += f"\n📄 {desc}\n"
if submission:
message += f"\n✅ Статус: Сдано\n"
if submission.status == 'graded' and submission.score is not None:
message += f"🎯 Оценка: {submission.score}/{hw.max_score}\n"
elif submission.status == 'returned':
message += f"🔄 Возвращено на доработку\n"
else:
message += f"⏳ Ожидает проверки\n"
else:
message += f"\n⏳ Статус: Не сдано\n"
# Создаем inline клавиатуру
inline_keyboard = []
if not submission or submission.status == 'returned':
inline_keyboard.append([
InlineKeyboardButton(
"📎 Загрузить решение",
callback_data=f"homework_upload_{hw.id}"
)
])
if submission:
inline_keyboard.append([
InlineKeyboardButton(
"👁️ Просмотреть решение",
callback_data=f"homework_view_{submission.id}"
)
])
inline_keyboard.append([
InlineKeyboardButton(
"🌐 Открыть на сайте",
url=f"{settings.FRONTEND_URL}/homework"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(message, parse_mode='HTML', reply_markup=reply_markup)
await update.message.reply_text("Используйте кнопки для навигации:", reply_markup=keyboard)
else:
# Если несколько заданий, показываем список с кнопками
message = "📝 Активные домашние задания:\n\n"
inline_keyboard = []
for hw in homeworks:
deadline_str = hw.deadline.strftime('%d.%m.%Y') if hw.deadline else 'Без дедлайна'
# Проверяем сдано ли
submission = await sync_to_async(
HomeworkSubmission.objects.filter(
homework=hw,
student=db_user
).first
)()
status = "✅ Сдано" if submission else "⏳ Не сдано"
message += f"{hw.title}\n"
message += f"📅 Дедлайн: {deadline_str}\n"
message += f"{status}\n\n"
# Кнопка для просмотра деталей
inline_keyboard.append([
InlineKeyboardButton(
f"📝 {hw.title[:30]}",
callback_data=f"homework_detail_{hw.id}"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(message, parse_mode='HTML', reply_markup=reply_markup)
await update.message.reply_text("Выберите задание для просмотра:", reply_markup=keyboard)
# Для родителя - показать задания всех детей
elif db_user.role == 'parent':
from apps.users.models import Parent
try:
parent_profile = await sync_to_async(lambda: db_user.parent_profile)()
children = await sync_to_async(list)(parent_profile.children.all())
if not children:
await update.message.reply_text(
"❌ У вас нет привязанных детей."
)
return
child_users = [child.user for child in children]
homeworks = await sync_to_async(list)(
Homework.objects.filter(
assigned_to__in=child_users,
deadline__gte=timezone.now()
).order_by('deadline')[:5]
)
if not homeworks:
await update.message.reply_text(
"📝 У ваших детей нет активных домашних заданий."
)
return
message = "📝 Домашние задания детей:\n\n"
for hw in homeworks:
deadline_str = hw.deadline.strftime('%d.%m.%Y') if hw.deadline else 'Без дедлайна'
# Находим для какого ребёнка это задание
child_students = [student for student in hw.assigned_to.all() if student in child_users]
child_names = ', '.join([child.get_full_name() for child in child_students[:2]])
if len(child_students) > 2:
child_names += f" и ещё {len(child_students) - 2}"
message += f"{hw.title}\n"
message += f"👶 {child_names}\n"
message += f"📅 Дедлайн: {deadline_str}\n\n"
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
logger.error(f"Error in homework_command for parent: {e}")
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения домашних заданий.",
reply_markup=keyboard
)
# Для ментора - показать задания требующие проверки
elif db_user.role == 'mentor':
submissions = await sync_to_async(list)(
HomeworkSubmission.objects.filter(
homework__mentor=db_user,
status='pending'
).order_by('-submitted_at')[:5]
)
if not submissions:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"📝 Нет домашних заданий, требующих проверки.",
reply_markup=keyboard
)
return
message = "📝 Домашние задания на проверку:\n\n"
for submission in submissions:
student_name = submission.student.get_full_name() if submission.student else 'Студент'
hw_title = submission.homework.title
submitted_at = submission.submitted_at.strftime('%d.%m.%Y') if submission.submitted_at else 'Неизвестно'
message += f"{hw_title}\n"
message += f"👤 {student_name}\n"
message += f"📅 Сдано: {submitted_at}\n\n"
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
else:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Эта команда доступна только для менторов, клиентов и родителей.",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error in homework_command: {e}")
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения домашних заданий.",
reply_markup=keyboard
)
async def _handle_text_homework_submission(self, update: Update, context: ContextTypes.DEFAULT_TYPE, db_user, homework_id: int, text_content: str):
"""
Обработка текстового решения домашнего задания.
Args:
update: Update объект от Telegram
context: Context объект
db_user: Пользователь из базы данных
homework_id: ID домашнего задания
text_content: Текстовое содержание решения
"""
try:
from apps.homework.models import Homework, HomeworkSubmission
from django.utils import timezone as tz
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
# Проверяем, что пользователь имеет право сдавать это ДЗ
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
await update.message.reply_text(
"❌ У вас нет доступа к этому заданию."
)
context.user_data.pop('waiting_for_homework_file', None)
return
# Проверяем, есть ли уже решение
existing_submission = await sync_to_async(
HomeworkSubmission.objects.filter(
homework=homework,
student=db_user
).order_by('-attempt_number').first
)()
if existing_submission and existing_submission.status != 'returned':
# Обновляем существующее решение
existing_submission.content = text_content
existing_submission.status = 'pending'
await sync_to_async(existing_submission.save)()
await update.message.reply_text(
f"✅ Решение обновлено!\n\n"
f"📝 Задание: {homework.title}\n\n"
f"Решение отправлено на проверку.",
parse_mode='HTML'
)
else:
# Определяем номер попытки
attempt_number = 1
if existing_submission:
attempt_number = existing_submission.attempt_number + 1
# Создаем новое решение
submission = HomeworkSubmission(
homework=homework,
student=db_user,
content=text_content,
status='pending',
attempt_number=attempt_number
)
await sync_to_async(submission.save)()
# Проверяем опоздание
await sync_to_async(submission.check_if_late)()
# Обновляем статистику задания
await sync_to_async(homework.update_statistics)()
await update.message.reply_text(
f"✅ Решение загружено!\n\n"
f"📝 Задание: {homework.title}\n\n"
f"Решение отправлено на проверку.",
parse_mode='HTML'
)
# Отправляем уведомление ментору
from apps.notifications.services import NotificationService
await sync_to_async(NotificationService.create_notification_with_telegram)(
recipient=homework.mentor,
notification_type='homework_submitted',
title='📝 ДЗ сдано',
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
priority='normal',
action_url=f'/homework/{homework.id}/',
content_object=homework
)
# Очищаем состояние ожидания
context.user_data.pop('waiting_for_homework_file', None)
keyboard = await get_user_keyboard(update.effective_user.id)
await update.message.reply_text(
"Используйте кнопки для навигации:",
reply_markup=keyboard
)
except Homework.DoesNotExist:
await update.message.reply_text(
"❌ Домашнее задание не найдено."
)
context.user_data.pop('waiting_for_homework_file', None)
except Exception as e:
logger.error(f"Error handling text homework submission: {e}", exc_info=True)
await update.message.reply_text(
"❌ Ошибка загрузки решения.\n\n"
"Попробуйте позже или обратитесь в поддержку."
)
context.user_data.pop('waiting_for_homework_file', None)
async def _refresh_settings_message(self, query, db_user, preferences):
"""Обновить сообщение с настройками."""
message_text = "⚙️ Настройки\n\n"
all_status = "✅ Включены" if preferences.enabled else "❌ Выключены"
telegram_status = "✅ Вкл" if preferences.telegram_enabled else "❌ Выкл"
email_status = "✅ Вкл" if preferences.email_enabled else "❌ Выкл"
in_app_status = "✅ Вкл" if preferences.in_app_enabled else "❌ Выкл"
message_text += f"🔔 Все уведомления: {all_status}\n"
message_text += f"📱 Telegram: {telegram_status}\n"
message_text += f"📧 Email: {email_status}\n"
message_text += f"💬 В приложении: {in_app_status}\n\n"
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
message_text += f"🔇 Режим тишины: {quiet_start} - {quiet_end}\n\n"
else:
message_text += "🔇 Режим тишины: ❌ Выключен\n\n"
user_tz = db_user.timezone or 'Europe/Moscow'
message_text += f"🕐 Часовой пояс: {user_tz}\n"
user_lang = db_user.language or 'ru'
lang_display = 'Русский' if user_lang == 'ru' else 'English'
message_text += f"🌐 Язык: {lang_display}\n"
keyboard = []
keyboard.append([
InlineKeyboardButton(
"🔔 " + ("Выключить все" if preferences.enabled else "Включить все"),
callback_data="settings_toggle_all"
)
])
keyboard.append([
InlineKeyboardButton(
"📱 Telegram: " + ("Выкл" if preferences.telegram_enabled else "Вкл"),
callback_data="settings_toggle_telegram"
),
InlineKeyboardButton(
"📧 Email: " + ("Выкл" if preferences.email_enabled else "Вкл"),
callback_data="settings_toggle_email"
)
])
keyboard.append([
InlineKeyboardButton("🔇 Режим тишины", callback_data="settings_quiet_hours"),
InlineKeyboardButton("📋 Типы уведомлений", callback_data="settings_notification_types")
])
keyboard.append([
InlineKeyboardButton("🕐 Часовой пояс", callback_data="settings_timezone"),
InlineKeyboardButton("🌐 Язык", callback_data="settings_language")
])
frontend_url = settings.FRONTEND_URL
if frontend_url and 'localhost' not in frontend_url and '127.0.0.1' not in frontend_url:
keyboard.append([
InlineKeyboardButton("🌐 Открыть на сайте", url=f"{frontend_url}/profile")
])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
async def _handle_quiet_hours(self, query, db_user, preferences):
"""Обработка настроек режима тишины."""
from datetime import time
message_text = "🔇 Режим тишины\n\n"
if preferences.quiet_hours_enabled and preferences.quiet_hours_start and preferences.quiet_hours_end:
quiet_start = preferences.quiet_hours_start.strftime('%H:%M')
quiet_end = preferences.quiet_hours_end.strftime('%H:%M')
message_text += f"Текущий: {quiet_start} - {quiet_end}\n\n"
else:
message_text += "Текущий: ❌ Выключен\n\n"
message_text += "Выберите действие:"
keyboard = []
# Предустановленные варианты
if not preferences.quiet_hours_enabled:
keyboard.append([
InlineKeyboardButton("✅ Включить (22:00 - 08:00)", callback_data="quiet_hours_enable_22_8")
])
keyboard.append([
InlineKeyboardButton("✅ Включить (23:00 - 07:00)", callback_data="quiet_hours_enable_23_7")
])
else:
keyboard.append([
InlineKeyboardButton("❌ Выключить", callback_data="quiet_hours_disable")
])
keyboard.append([
InlineKeyboardButton("🕐 Изменить время", callback_data="quiet_hours_custom")
])
keyboard.append([
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
async def _handle_notification_types(self, query, db_user, preferences):
"""Обработка настроек типов уведомлений."""
from .models import Notification
# Показываем меню выбора типов уведомлений
message_text = "📋 Типы уведомлений\n\n"
message_text += "Выберите типы уведомлений для Telegram:\n\n"
# Группируем типы по категориям
lesson_types = [t for t in Notification.TYPE_CHOICES if 'lesson' in t[0]]
homework_types = [t for t in Notification.TYPE_CHOICES if 'homework' in t[0]]
other_types = [t for t in Notification.TYPE_CHOICES if 'lesson' not in t[0] and 'homework' not in t[0]]
keyboard = []
# Кнопки для занятий
if lesson_types:
keyboard.append([InlineKeyboardButton("📅 Занятия", callback_data="noop")])
for ntype, display in lesson_types:
type_prefs = preferences.type_preferences.get(ntype, {})
if not isinstance(type_prefs, dict):
type_prefs = {}
enabled = type_prefs.get('telegram', True)
status = "✅" if enabled else "❌"
# Сокращаем длинные названия
short_display = display.replace('Занятие ', '').replace(' занятие', '')
keyboard.append([
InlineKeyboardButton(
f"{status} {short_display}",
callback_data=f"toggle_type_{ntype}"
)
])
# Кнопки для домашних заданий
if homework_types:
keyboard.append([InlineKeyboardButton("📝 Домашние задания", callback_data="noop")])
for ntype, display in homework_types:
type_prefs = preferences.type_preferences.get(ntype, {})
if not isinstance(type_prefs, dict):
type_prefs = {}
enabled = type_prefs.get('telegram', True)
status = "✅" if enabled else "❌"
short_display = display.replace('Домашнее задание', 'ДЗ')
keyboard.append([
InlineKeyboardButton(
f"{status} {short_display}",
callback_data=f"toggle_type_{ntype}"
)
])
# Кнопки для других типов (первые 3)
if other_types:
keyboard.append([InlineKeyboardButton("📢 Другие", callback_data="noop")])
for ntype, display in other_types[:3]:
type_prefs = preferences.type_preferences.get(ntype, {})
if not isinstance(type_prefs, dict):
type_prefs = {}
enabled = type_prefs.get('telegram', True)
status = "✅" if enabled else "❌"
keyboard.append([
InlineKeyboardButton(
f"{status} {display}",
callback_data=f"toggle_type_{ntype}"
)
])
keyboard.append([
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
async def _handle_timezone(self, query, db_user):
"""Обработка настройки часового пояса."""
# Популярные часовые пояса
popular_timezones = [
('Europe/Moscow', 'Москва (UTC+3)'),
('Europe/Kiev', 'Киев (UTC+2)'),
('Asia/Almaty', 'Алматы (UTC+6)'),
('Europe/Minsk', 'Минск (UTC+3)'),
('Asia/Tashkent', 'Ташкент (UTC+5)'),
('Asia/Yekaterinburg', 'Екатеринбург (UTC+5)'),
('Asia/Novosibirsk', 'Новосибирск (UTC+7)'),
('Europe/Kaliningrad', 'Калининград (UTC+2)'),
]
message_text = "🕐 Часовой пояс\n\n"
message_text += "Текущий: " + (db_user.timezone or 'Europe/Moscow') + "\n\n"
message_text += "Выберите часовой пояс:"
keyboard = []
for tz, name in popular_timezones:
keyboard.append([
InlineKeyboardButton(
name + (" ✅" if db_user.timezone == tz else ""),
callback_data=f"set_timezone_{tz}"
)
])
keyboard.append([
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
async def _handle_language(self, query, db_user):
"""Обработка настройки языка."""
message_text = "🌐 Язык интерфейса\n\n"
message_text += "Текущий: " + ('Русский' if db_user.language == 'ru' else 'English') + "\n\n"
message_text += "Выберите язык:"
keyboard = [
[
InlineKeyboardButton(
"Русский" + (" ✅" if db_user.language == 'ru' else ""),
callback_data="set_language_ru"
),
InlineKeyboardButton(
"English" + (" ✅" if db_user.language == 'en' else ""),
callback_data="set_language_en"
)
],
[
InlineKeyboardButton("◀️ Назад к настройкам", callback_data="settings_back")
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(message_text, parse_mode='HTML', reply_markup=reply_markup)
async def clients_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /clients - список клиентов ментора."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User, Client
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
keyboard = get_main_keyboard(None)
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания аккаунта.",
reply_markup=keyboard
)
return
if db_user.role != 'mentor':
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Эта команда доступна только для менторов.",
reply_markup=keyboard
)
return
# Получаем клиентов ментора
clients = await sync_to_async(list)(
Client.objects.filter(mentors=db_user).select_related('user')[:10]
)
if not clients:
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"👥 У вас пока нет клиентов.\n\n"
"Клиенты появятся здесь после того, как они запишутся на ваши занятия.",
reply_markup=keyboard
)
return
message = "👥 Ваши клиенты:\n\n"
from apps.schedule.models import Lesson
from apps.homework.models import HomeworkSubmission
from django.utils import timezone
inline_keyboard = []
for client in clients:
client_name = client.user.get_full_name() or client.user.email
message += f"👤 {client_name}\n"
# Статистика занятий
lessons = await sync_to_async(list)(
Lesson.objects.filter(
mentor=db_user,
client=client
)
)
total_lessons = len(lessons)
completed_lessons = len([l for l in lessons if l.status == 'completed'])
upcoming_lessons = len([
l for l in lessons
if l.start_time >= timezone.now() and l.status == 'scheduled'
])
message += f"📚 Занятий: {total_lessons} (завершено: {completed_lessons}, предстоящих: {upcoming_lessons})\n"
# Статистика ДЗ
submissions = await sync_to_async(list)(
HomeworkSubmission.objects.filter(
homework__mentor=db_user,
student=client.user
)
)
total_homeworks = len(submissions)
graded_homeworks = len([s for s in submissions if s.status == 'graded'])
if graded_homeworks > 0:
scores = [s.score for s in submissions if s.score is not None]
avg_score = sum(scores) / len(scores) if scores else 0
message += f"📝 ДЗ: {total_homeworks} (проверено: {graded_homeworks}, средний балл: {avg_score:.1f})\n"
else:
message += f"📝 ДЗ: {total_homeworks}\n"
message += "\n"
# Добавляем кнопку для просмотра деталей клиента
inline_keyboard.append([
InlineKeyboardButton(
f"👤 {client_name[:30]}",
callback_data=f"client_detail_{client.id}"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard) if inline_keyboard else None
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(message, parse_mode='HTML', reply_markup=reply_markup)
except Exception as e:
logger.error(f"Error in clients_command: {e}", exc_info=True)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения списка клиентов.\n\n"
"Попробуйте позже или обратитесь в поддержку.",
reply_markup=keyboard
)
async def stats_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /stats - статистика для ментора."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User, Client
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
keyboard = get_main_keyboard(None)
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания аккаунта.",
reply_markup=keyboard
)
return
if db_user.role != 'mentor':
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Эта команда доступна только для менторов.",
reply_markup=keyboard
)
return
from apps.schedule.models import Lesson
from apps.homework.models import Homework, HomeworkSubmission
from django.utils import timezone
from datetime import timedelta
now = timezone.now()
month_ago = now - timedelta(days=30)
# Общая статистика
total_clients = await sync_to_async(Client.objects.filter(mentors=db_user).count)()
# Занятия
all_lessons = await sync_to_async(list)(
Lesson.objects.filter(mentor=db_user)
)
total_lessons = len(all_lessons)
completed_lessons = len([l for l in all_lessons if l.status == 'completed'])
upcoming_lessons = len([
l for l in all_lessons
if l.start_time >= now and l.status == 'scheduled'
])
lessons_this_month = len([
l for l in all_lessons
if l.start_time >= month_ago
])
# Домашние задания
all_submissions = await sync_to_async(list)(
HomeworkSubmission.objects.filter(homework__mentor=db_user)
)
total_homeworks = len(all_submissions)
pending_homeworks = len([s for s in all_submissions if s.status == 'pending'])
graded_homeworks = len([s for s in all_submissions if s.status == 'graded'])
if graded_homeworks > 0:
scores = [s.score for s in all_submissions if s.score is not None]
avg_score = sum(scores) / len(scores) if scores else 0
else:
avg_score = 0
message = "📊 Ваша статистика:\n\n"
message += f"👥 Клиентов: {total_clients}\n\n"
message += f"📚 Занятия:\n"
message += f"• Всего: {total_lessons}\n"
message += f"• Завершено: {completed_lessons}\n"
message += f"• Предстоящих: {upcoming_lessons}\n"
message += f"• За месяц: {lessons_this_month}\n\n"
message += f"📝 Домашние задания:\n"
message += f"• Всего решений: {total_homeworks}\n"
message += f"• На проверке: {pending_homeworks}\n"
message += f"• Проверено: {graded_homeworks}\n"
if avg_score > 0:
message += f"• Средний балл: {avg_score:.1f}\n"
# Доходы (если есть занятия с ценой)
lessons_with_price = [l for l in all_lessons if l.price and l.status == 'completed']
if lessons_with_price:
total_revenue = sum([float(l.price) for l in lessons_with_price])
avg_price = total_revenue / len(lessons_with_price) if lessons_with_price else 0
revenue_this_month = sum([
float(l.price) for l in lessons_with_price
if l.start_time >= month_ago
])
message += f"\n💰 Доходы:\n"
message += f"• Всего: {total_revenue:.2f} ₽\n"
message += f"• Средняя цена: {avg_price:.2f} ₽\n"
message += f"• За месяц: {revenue_this_month:.2f} ₽\n"
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
logger.error(f"Error in stats_command: {e}", exc_info=True)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения статистики.\n\n"
"Попробуйте позже или обратитесь в поддержку.",
reply_markup=keyboard
)
async def progress_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /progress - прогресс обучения для клиента."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User, Client
try:
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
keyboard = get_main_keyboard(None)
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания аккаунта.",
reply_markup=keyboard
)
return
if db_user.role != 'client':
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Эта команда доступна только для клиентов.",
reply_markup=keyboard
)
return
from apps.schedule.models import Lesson
from apps.homework.models import HomeworkSubmission
from django.utils import timezone
from datetime import timedelta
now = timezone.now()
month_ago = now - timedelta(days=30)
# Занятия за последний месяц
lessons = await sync_to_async(list)(
Lesson.objects.filter(
client__user=db_user,
start_time__gte=month_ago
)
)
total_lessons = len(lessons)
completed_lessons = len([l for l in lessons if l.status == 'completed'])
completion_rate = (completed_lessons / total_lessons * 100) if total_lessons > 0 else 0
# Домашние задания за последний месяц
submissions = await sync_to_async(list)(
HomeworkSubmission.objects.filter(
student=db_user,
submitted_at__gte=month_ago
)
)
graded_submissions = [s for s in submissions if s.status == 'graded']
passed_submissions = len([s for s in graded_submissions if s.passed])
total_graded = len(graded_submissions)
pass_rate = (passed_submissions / total_graded * 100) if total_graded > 0 else 0
# Средний балл
if graded_submissions:
scores = [s.score for s in graded_submissions if s.score is not None]
avg_score = sum(scores) / len(scores) if scores else 0
else:
avg_score = 0
# Всего занятий (все время)
all_lessons = await sync_to_async(list)(
Lesson.objects.filter(client__user=db_user)
)
total_all_lessons = len(all_lessons)
completed_all_lessons = len([l for l in all_lessons if l.status == 'completed'])
# Всего ДЗ (все время)
all_submissions = await sync_to_async(list)(
HomeworkSubmission.objects.filter(student=db_user)
)
total_all_homeworks = len(all_submissions)
graded_all_homeworks = len([s for s in all_submissions if s.status == 'graded'])
message = "📊 Ваш прогресс:\n\n"
message += f"📅 За последний месяц:\n"
message += f"• Занятий: {total_lessons} (завершено: {completed_lessons})\n"
message += f"• Процент завершения: {completion_rate:.1f}%\n"
message += f"• ДЗ сдано: {len(submissions)} (проверено: {total_graded})\n"
if total_graded > 0:
message += f"• Процент сдачи: {pass_rate:.1f}%\n"
message += f"• Средний балл: {avg_score:.1f}\n"
message += f"\n📈 Всего:\n"
message += f"• Занятий: {total_all_lessons} (завершено: {completed_all_lessons})\n"
message += f"• ДЗ сдано: {total_all_homeworks} (проверено: {graded_all_homeworks})\n"
# Топ предметы
if completed_lessons > 0:
from collections import Counter
subjects = [l.subject.name if l.subject else 'Без предмета' for l in lessons if l.status == 'completed' and l.subject]
if subjects:
subject_counts = Counter(subjects)
top_subjects = subject_counts.most_common(3)
message += f"\n📚 Топ предметы (месяц):\n"
for subject, count in top_subjects:
message += f"• {subject}: {count} занятий\n"
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(message, parse_mode='HTML', reply_markup=keyboard)
except Exception as e:
logger.error(f"Error in progress_command: {e}", exc_info=True)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Ошибка получения прогресса.\n\n"
"Попробуйте позже или обратитесь в поддержку.",
reply_markup=keyboard
)
async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик обычных сообщений и кнопок."""
user = update.effective_user
telegram_id = user.id
message_text = update.message.text
from apps.users.models import User
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
# Проверяем, ожидается ли загрузка решения для ДЗ
if db_user:
homework_id = context.user_data.get('waiting_for_homework_file')
if homework_id:
# Если пользователь отправил текст "отмена", отменяем загрузку
if message_text and message_text.lower() in ['отмена', 'cancel', '/cancel']:
context.user_data.pop('waiting_for_homework_file', None)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"❌ Загрузка решения отменена.",
reply_markup=keyboard
)
return
else:
# Обрабатываем текстовое решение
await self._handle_text_homework_submission(update, context, db_user, homework_id, message_text)
return
# Обработка нажатий на кнопки
if message_text == "📅 Расписание" or message_text == "📅 Моё расписание" or message_text == "📅 Расписание детей":
await self.schedule_command(update, context)
elif message_text == "📚 Следующее занятие":
await self.nextlesson_command(update, context)
elif message_text == "📝 Домашние задания" or message_text == "📝 Мои задания" or message_text == "📝 Задания детей":
await self.homework_command(update, context)
elif message_text == "📊 Мой прогресс":
await self.progress_command(update, context)
elif message_text == "👥 Клиенты":
await self.clients_command(update, context)
elif message_text == "📊 Статистика":
await self.stats_command(update, context)
elif message_text == "⚙️ Настройки":
await self.settings_command(update, context)
elif message_text == "ℹ️ Статус":
await self.status_command(update, context)
elif message_text == "❓ Помощь":
await self.help_command(update, context)
elif message_text == "🔗 Связать аккаунт":
await update.message.reply_text(
"🔗 Связывание аккаунта\n\n"
"1. Войдите на платформу\n"
"2. Перейдите в Профиль → Настройки → Telegram\n"
"3. Нажмите \"Связать Telegram\"\n"
"4. Отправьте команду: /link ВАШ_КОД\n\n"
"Или используйте команду:\n"
"/link <ваш_код_связывания>",
parse_mode='HTML'
)
else:
# Если аккаунт связан, показываем клавиатуру
if db_user:
keyboard = get_main_keyboard(db_user.role)
await update.message.reply_text(
"Используйте кнопки или команды для работы с ботом.\n\n"
"Введите /help для списка команд.",
reply_markup=keyboard
)
else:
keyboard = get_main_keyboard(None)
await update.message.reply_text(
"Используйте кнопки или команды для работы с ботом.\n\n"
"Введите /help для списка команд.",
reply_markup=keyboard
)
async def handle_document(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик загрузки документов (для решений ДЗ)."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания аккаунта."
)
return
# Проверяем, ожидается ли загрузка файла для ДЗ
homework_id = context.user_data.get('waiting_for_homework_file')
if not homework_id:
await update.message.reply_text(
"📎 Чтобы загрузить файл для решения ДЗ:\n\n"
"1. Используйте команду /homework\n"
"2. Выберите задание\n"
"3. Нажмите кнопку '📎 Загрузить решение'"
)
return
# Получаем файл
document = update.message.document
if not document:
await update.message.reply_text("❌ Файл не найден.")
return
try:
# Скачиваем файл из Telegram
from telegram import Bot
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
file = await bot.get_file(document.file_id)
# Получаем домашнее задание для проверок
from apps.homework.models import Homework, HomeworkSubmission
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
# Безопасность: Проверка размера файла (максимум 50MB для Telegram, но проверяем и настройки задания)
MAX_TELEGRAM_FILE_SIZE = 50 * 1024 * 1024 # 50 MB - максимум для Telegram
file_size = document.file_size or 0
if file_size > MAX_TELEGRAM_FILE_SIZE:
await update.message.reply_text(
f"❌ Файл слишком большой. Максимальный размер: {MAX_TELEGRAM_FILE_SIZE / (1024*1024):.0f} MB"
)
await bot.close()
return
# Проверяем размер файла согласно настройкам задания
if homework.max_file_size > 0 and file_size > homework.max_file_size:
max_size_mb = homework.max_file_size / (1024 * 1024)
await update.message.reply_text(
f"❌ Файл слишком большой. Максимальный размер для этого задания: {max_size_mb:.1f} MB"
)
await bot.close()
return
# Безопасность: Проверка типа файла
from apps.homework.utils import validate_file_type, sanitize_filename
safe_filename = sanitize_filename(document.file_name or 'file')
if homework.allowed_file_types and not validate_file_type(safe_filename, homework.allowed_file_types):
allowed = homework.allowed_file_types.replace(',', ', ')
await update.message.reply_text(
f"❌ Тип файла не разрешен.\n\n"
f"Разрешенные типы: {allowed}"
)
await bot.close()
return
# Создаем временный файл
import tempfile
import os
from django.core.files import File
file_ext = os.path.splitext(safe_filename)[1]
tmp_file_path = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=file_ext) as tmp_file:
tmp_file_path = tmp_file.name
await file.download_to_drive(tmp_file_path)
# Проверяем, что пользователь имеет право сдавать это ДЗ
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
await update.message.reply_text(
"❌ У вас нет доступа к этому заданию."
)
await bot.close()
return
# Проверяем, есть ли уже решение
existing_submission = await sync_to_async(
HomeworkSubmission.objects.filter(
homework=homework,
student=db_user
).order_by('-attempt_number').first
)()
if existing_submission and existing_submission.status != 'returned':
# Обновляем существующее решение
with open(tmp_file_path, 'rb') as f:
django_file = File(f, name=safe_filename)
existing_submission.attachment = django_file
existing_submission.content = f"Решение загружено через Telegram: {safe_filename}"
existing_submission.status = 'pending'
await sync_to_async(existing_submission.save)()
await update.message.reply_text(
f"✅ Решение обновлено!\n\n"
f"📎 Файл: {safe_filename}\n"
f"📝 Задание: {homework.title}\n\n"
f"Решение отправлено на проверку.",
parse_mode='HTML'
)
else:
# Определяем номер попытки
attempt_number = 1
if existing_submission:
attempt_number = existing_submission.attempt_number + 1
# Создаем новое решение
with open(tmp_file_path, 'rb') as f:
django_file = File(f, name=safe_filename)
submission = HomeworkSubmission(
homework=homework,
student=db_user,
content=f"Решение загружено через Telegram: {safe_filename}",
attachment=django_file,
status='pending',
attempt_number=attempt_number
)
await sync_to_async(submission.save)()
# Проверяем опоздание
await sync_to_async(submission.check_if_late)()
# Обновляем статистику задания
await sync_to_async(homework.update_statistics)()
await update.message.reply_text(
f"✅ Решение загружено!\n\n"
f"📎 Файл: {safe_filename}\n"
f"📝 Задание: {homework.title}\n\n"
f"Решение отправлено на проверку.",
parse_mode='HTML'
)
# Отправляем уведомление ментору
from apps.notifications.services import NotificationService
await sync_to_async(NotificationService.create_notification_with_telegram)(
recipient=homework.mentor,
notification_type='homework_submitted',
title='📝 ДЗ сдано',
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
priority='normal',
action_url=f'/homework/{homework.id}/',
content_object=homework
)
# Очищаем состояние ожидания
context.user_data.pop('waiting_for_homework_file', None)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"Используйте кнопки для навигации:",
reply_markup=keyboard
)
finally:
# Гарантированно удаляем временный файл
if tmp_file_path and os.path.exists(tmp_file_path):
try:
os.unlink(tmp_file_path)
except Exception as e:
logger.error(f"Error deleting temp file {tmp_file_path}: {e}")
await bot.close()
except Homework.DoesNotExist:
await update.message.reply_text(
"❌ Домашнее задание не найдено."
)
context.user_data.pop('waiting_for_homework_file', None)
except Exception as e:
logger.error(f"Error handling document upload: {e}", exc_info=True)
await update.message.reply_text(
"❌ Ошибка загрузки файла.\n\n"
"Попробуйте позже или обратитесь в поддержку."
)
context.user_data.pop('waiting_for_homework_file', None)
async def handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик загрузки фото (для решений ДЗ)."""
user = update.effective_user
telegram_id = user.id
from apps.users.models import User
db_user = await sync_to_async(User.objects.filter(telegram_id=telegram_id).first)()
if not db_user:
await update.message.reply_text(
"❌ Аккаунт не связан.\n\n"
"Используйте /link <код> для связывания аккаунта."
)
return
# Проверяем, ожидается ли загрузка файла для ДЗ
homework_id = context.user_data.get('waiting_for_homework_file')
if not homework_id:
await update.message.reply_text(
"📎 Чтобы загрузить фото для решения ДЗ:\n\n"
"1. Используйте команду /homework\n"
"2. Выберите задание\n"
"3. Нажмите кнопку '📎 Загрузить решение'"
)
return
# Получаем фото (берем самое большое)
photos = update.message.photo
if not photos:
await update.message.reply_text("❌ Фото не найдено.")
return
photo = photos[-1] # Самое большое фото
try:
# Скачиваем фото из Telegram
from telegram import Bot
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
file = await bot.get_file(photo.file_id)
# Получаем домашнее задание для проверок
from apps.homework.models import Homework, HomeworkSubmission
homework = await sync_to_async(Homework.objects.get)(id=homework_id)
# Безопасность: Проверка размера файла (максимум 50MB для Telegram)
MAX_TELEGRAM_FILE_SIZE = 50 * 1024 * 1024 # 50 MB - максимум для Telegram
file_size = photo.file_size or 0
if file_size > MAX_TELEGRAM_FILE_SIZE:
await update.message.reply_text(
f"❌ Фото слишком большое. Максимальный размер: {MAX_TELEGRAM_FILE_SIZE / (1024*1024):.0f} MB"
)
await bot.close()
return
# Проверяем размер файла согласно настройкам задания
if homework.max_file_size > 0 and file_size > homework.max_file_size:
max_size_mb = homework.max_file_size / (1024 * 1024)
await update.message.reply_text(
f"❌ Фото слишком большое. Максимальный размер для этого задания: {max_size_mb:.1f} MB"
)
await bot.close()
return
# Безопасность: Проверка типа файла (фото должно быть разрешено)
from apps.homework.utils import validate_file_type, sanitize_filename
if homework.allowed_file_types and not validate_file_type('photo.jpg', homework.allowed_file_types):
allowed = homework.allowed_file_types.replace(',', ', ')
await update.message.reply_text(
f"❌ Фото не разрешено для этого задания.\n\n"
f"Разрешенные типы: {allowed}"
)
await bot.close()
return
# Создаем временный файл
import tempfile
import os
from django.core.files import File
tmp_file_path = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_file:
tmp_file_path = tmp_file.name
await file.download_to_drive(tmp_file_path)
# Проверяем, что пользователь имеет право сдавать это ДЗ
if db_user.role != 'client' or db_user not in await sync_to_async(list)(homework.assigned_to.all()):
await update.message.reply_text(
"❌ У вас нет доступа к этому заданию."
)
await bot.close()
return
from django.utils import timezone as tz
from apps.homework.utils import sanitize_filename
filename = sanitize_filename(f"photo_{homework.id}_{tz.now().strftime('%Y%m%d_%H%M%S')}.jpg")
# Проверяем, есть ли уже решение
existing_submission = await sync_to_async(
HomeworkSubmission.objects.filter(
homework=homework,
student=db_user
).order_by('-attempt_number').first
)()
if existing_submission and existing_submission.status != 'returned':
# Обновляем существующее решение
with open(tmp_file_path, 'rb') as f:
django_file = File(f, name=filename)
existing_submission.attachment = django_file
existing_submission.content = f"Решение загружено через Telegram (фото)"
existing_submission.status = 'pending'
await sync_to_async(existing_submission.save)()
await update.message.reply_text(
f"✅ Решение обновлено!\n\n"
f"📎 Фото загружено\n"
f"📝 Задание: {homework.title}\n\n"
f"Решение отправлено на проверку.",
parse_mode='HTML'
)
else:
# Определяем номер попытки
attempt_number = 1
if existing_submission:
attempt_number = existing_submission.attempt_number + 1
# Создаем новое решение
with open(tmp_file_path, 'rb') as f:
django_file = File(f, name=filename)
submission = HomeworkSubmission(
homework=homework,
student=db_user,
content="Решение загружено через Telegram (фото)",
attachment=django_file,
status='pending',
attempt_number=attempt_number
)
await sync_to_async(submission.save)()
# Проверяем опоздание
await sync_to_async(submission.check_if_late)()
# Обновляем статистику задания
await sync_to_async(homework.update_statistics)()
await update.message.reply_text(
f"✅ Решение загружено!\n\n"
f"📎 Фото загружено\n"
f"📝 Задание: {homework.title}\n\n"
f"Решение отправлено на проверку.",
parse_mode='HTML'
)
# Отправляем уведомление ментору
from apps.notifications.services import NotificationService
await sync_to_async(NotificationService.create_notification_with_telegram)(
recipient=homework.mentor,
notification_type='homework_submitted',
title='📝 ДЗ сдано',
message=f'{db_user.get_full_name()} сдал ДЗ "{homework.title}"',
priority='normal',
action_url=f'/homework/{homework.id}/',
content_object=homework
)
# Очищаем состояние ожидания
context.user_data.pop('waiting_for_homework_file', None)
keyboard = await get_user_keyboard(telegram_id)
await update.message.reply_text(
"Используйте кнопки для навигации:",
reply_markup=keyboard
)
finally:
# Гарантированно удаляем временный файл
if tmp_file_path and os.path.exists(tmp_file_path):
try:
os.unlink(tmp_file_path)
except Exception as e:
logger.error(f"Error deleting temp file {tmp_file_path}: {e}")
await bot.close()
except Homework.DoesNotExist:
await update.message.reply_text(
"❌ Домашнее задание не найдено."
)
context.user_data.pop('waiting_for_homework_file', None)
except Exception as e:
logger.error(f"Error handling photo upload: {e}", exc_info=True)
await update.message.reply_text(
"❌ Ошибка загрузки фото.\n\n"
"Попробуйте позже или обратитесь в поддержку."
)
context.user_data.pop('waiting_for_homework_file', None)
def setup_handlers(self):
"""Настройка обработчиков команд."""
if not self.application:
return
# Команды
self.application.add_handler(CommandHandler("start", self.start_command))
self.application.add_handler(CommandHandler("help", self.help_command))
self.application.add_handler(CommandHandler("link", self.link_command))
self.application.add_handler(CommandHandler("unlink", self.unlink_command))
self.application.add_handler(CommandHandler("status", self.status_command))
self.application.add_handler(CommandHandler("settings", self.settings_command))
# Команды для клиентов и менторов
self.application.add_handler(CommandHandler("schedule", self.schedule_command))
self.application.add_handler(CommandHandler("nextlesson", self.nextlesson_command))
self.application.add_handler(CommandHandler("homework", self.homework_command))
# Команды для клиентов
self.application.add_handler(CommandHandler("progress", self.progress_command))
# Команды для менторов
self.application.add_handler(CommandHandler("clients", self.clients_command))
self.application.add_handler(CommandHandler("stats", self.stats_command))
# Кнопки
self.application.add_handler(CallbackQueryHandler(self.button_callback))
# Обычные сообщения
self.application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self.handle_message))
# Загрузка документов (для решений ДЗ)
self.application.add_handler(MessageHandler(filters.Document.ALL, self.handle_document))
# Загрузка фото (для решений ДЗ)
self.application.add_handler(MessageHandler(filters.PHOTO, self.handle_photo))
async def start(self):
"""Запуск бота."""
if not self.token:
logger.error("TELEGRAM_BOT_TOKEN not set")
return
logger.info("Starting Telegram bot...")
# Создаем приложение
self.application = Application.builder().token(self.token).build()
# Настраиваем обработчики
self.setup_handlers()
# Запускаем бота
await self.application.initialize()
await self.application.start()
if self.use_webhook and self.webhook_url:
# Используем webhook режим
logger.info(f"Setting up webhook: {self.webhook_url}")
await self.setup_webhook()
else:
# Используем polling режим
logger.info("Starting bot in polling mode...")
await self.application.updater.start_polling()
logger.info("Telegram bot started successfully")
async def setup_webhook(self):
"""Настройка webhook для бота."""
from telegram import Bot
from telegram.error import TelegramError
try:
bot = Bot(token=self.token)
# Устанавливаем webhook
webhook_kwargs = {
'url': self.webhook_url,
'allowed_updates': ['message', 'callback_query', 'inline_query', 'chosen_inline_result'],
}
# Добавляем secret token если указан
if self.webhook_secret_token:
webhook_kwargs['secret_token'] = self.webhook_secret_token
await bot.set_webhook(**webhook_kwargs)
# Проверяем информацию о webhook
webhook_info = await bot.get_webhook_info()
logger.info(f"Webhook info: {webhook_info.url}, pending updates: {webhook_info.pending_update_count}")
await bot.close()
logger.info("Webhook set successfully")
except TelegramError as e:
logger.error(f"Error setting webhook: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error setting webhook: {e}")
raise
async def remove_webhook(self):
"""Удаление webhook."""
from telegram import Bot
from telegram.error import TelegramError
try:
bot = Bot(token=self.token)
await bot.delete_webhook(drop_pending_updates=True)
await bot.close()
logger.info("Webhook removed successfully")
except TelegramError as e:
logger.error(f"Error removing webhook: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error removing webhook: {e}")
raise
async def stop(self):
"""Остановка бота."""
if self.application:
logger.info("Stopping Telegram bot...")
if not self.use_webhook:
# Останавливаем polling только если не используем webhook
await self.application.updater.stop()
await self.application.stop()
await self.application.shutdown()
logger.info("Telegram bot stopped")
def get_application(self):
"""
Получить экземпляр Application для обработки webhook.
ВАЖНО: Этот метод создает приложение, но не инициализирует его.
Инициализация происходит в process_webhook_update при первом запросе.
"""
if not self.application:
if not self.token:
logger.error("TELEGRAM_BOT_TOKEN not set")
return None
# Создаем приложение
self.application = Application.builder().token(self.token).build()
# Настраиваем обработчики
self.setup_handlers()
logger.info("Bot application created for webhook")
return self.application
async def process_webhook_update(self, update: Update):
"""
Обработать update от webhook.
Args:
update: Update объект от Telegram
"""
# Получаем или создаем приложение
if not self.application:
self.get_application()
if not self.application:
logger.error("Failed to initialize bot application")
return False
try:
# Убеждаемся что приложение инициализировано и запущено
# Для webhook режима мы не используем updater, только application
if not hasattr(self.application, '_webhook_initialized'):
await self.application.initialize()
await self.application.start()
self.application._webhook_initialized = True
logger.info("Bot application initialized for webhook")
# Обрабатываем update
await self.application.process_update(update)
return True
except Exception as e:
logger.error(f"Error processing webhook update: {e}", exc_info=True)
return False
# Глобальный экземпляр бота
bot_instance = None
async def get_bot():
"""Получить экземпляр бота."""
global bot_instance
if bot_instance is None:
bot_instance = TelegramBot()
await bot_instance.start()
return bot_instance
async def send_telegram_message(telegram_id: int, message: str, parse_mode: str = 'HTML'):
"""
Отправить сообщение в Telegram.
Args:
telegram_id: ID пользователя в Telegram
message: Текст сообщения
parse_mode: Режим парсинга (HTML, Markdown)
"""
from telegram import Bot
from telegram.error import TelegramError
if not settings.TELEGRAM_BOT_TOKEN:
logger.error("TELEGRAM_BOT_TOKEN not set")
return False
try:
# Создаем временный экземпляр бота для отправки сообщения
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
await bot.send_message(
chat_id=telegram_id,
text=message,
parse_mode=parse_mode
)
await bot.close()
logger.info(f"Telegram message sent to {telegram_id}")
return True
except TelegramError as e:
logger.error(f"Telegram API error sending message to {telegram_id}: {e}")
return False
except Exception as e:
logger.error(f"Error sending Telegram message to {telegram_id}: {e}")
return False
async def send_telegram_message_with_buttons(telegram_id: int, message: str, reply_markup, parse_mode: str = 'HTML'):
"""
Отправить сообщение в Telegram с кнопками.
Args:
telegram_id: ID пользователя в Telegram
message: Текст сообщения
reply_markup: InlineKeyboardMarkup с кнопками
parse_mode: Режим парсинга (HTML, Markdown)
"""
from telegram import Bot
from telegram.error import TelegramError
if not settings.TELEGRAM_BOT_TOKEN:
logger.error("TELEGRAM_BOT_TOKEN not set")
return False
try:
bot = Bot(token=settings.TELEGRAM_BOT_TOKEN)
await bot.send_message(
chat_id=telegram_id,
text=message,
parse_mode=parse_mode,
reply_markup=reply_markup
)
await bot.close()
logger.info(f"Telegram message with buttons sent to {telegram_id}")
return True
except TelegramError as e:
logger.error(f"Telegram API error sending message with buttons to {telegram_id}: {e}")
return False
except Exception as e:
logger.error(f"Error sending Telegram message with buttons to {telegram_id}: {e}")
return False