scheduleSon/backend/telegram_bot.py

765 lines
32 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (
Application, CommandHandler, CallbackQueryHandler, MessageHandler,
ContextTypes, ConversationHandler, filters
)
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from datetime import datetime, timedelta
import pytz
import os
import asyncio
import logging
from backend.database import AsyncSessionLocal, Task, Event, TelegramUser
from backend.utils import get_week_range, format_date_russian, check_event_overlap, get_weekday_from_date
from backend.models import TaskCreate, EventCreate
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
TZ = pytz.timezone("Europe/Moscow")
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
# Состояния для ConversationHandler
SELECTING_DATE, SELECTING_HOUR, SELECTING_MINUTE, SELECTING_DURATION_HOUR, SELECTING_DURATION_MIN, ENTERING_TITLE = range(6)
SELECTING_TASK_DATE, ENTERING_TASK_TITLE, SELECTING_REPEAT, SELECTING_COPY_DAYS = range(10, 14)
SELECTING_DELETE_DATE, SELECTING_DELETE_ITEM = range(20, 22)
user_states = {} # Временное хранилище состояний пользователей
async def register_user(chat_id: str):
"""Зарегистрировать пользователя для получения напоминаний"""
async with AsyncSessionLocal() as db:
result = await db.execute(
select(TelegramUser).where(TelegramUser.chat_id == chat_id)
)
user = result.scalar_one_or_none()
if not user:
user = TelegramUser(chat_id=chat_id)
db.add(user)
await db.commit()
async def get_schedule_for_date(date_str: str, add_prefix: bool = False) -> str:
"""Получить расписание на дату в текстовом формате"""
async with AsyncSessionLocal() as db:
# Получаем tasks (разовые)
result = await db.execute(
select(Task).where(
and_(Task.date == date_str, Task.repeat_weekly == False)
)
)
tasks = result.scalars().all()
# Получаем weekly tasks для этой даты
from backend.utils import materialize_weekly_tasks
weekly_items = await materialize_weekly_tasks(db, date_str, date_str)
weekly_tasks = [item for item in weekly_items if item.kind == "task"]
# Получаем events
result = await db.execute(
select(Event).where(Event.date == date_str)
)
events = result.scalars().all()
events = sorted(events, key=lambda e: e.start_time)
# Определяем префикс для даты
date_prefix = ""
if add_prefix:
today = datetime.now(TZ).date()
tomorrow = today + timedelta(days=1)
date_obj = datetime.strptime(date_str, "%Y-%m-%d").date()
if date_obj == today:
date_prefix = "Сегодня "
elif date_obj == tomorrow:
date_prefix = "Завтра "
text = f"📅 {date_prefix}{format_date_russian(date_str)}\n\n"
# Объединяем tasks
all_tasks = []
for task in tasks:
all_tasks.append(task.title)
for task_item in weekly_tasks:
all_tasks.append(task_item.title)
if all_tasks:
text += "📋 Задачи:\n"
for title in all_tasks:
text += f"{title}\n"
text += "\n"
if events:
text += "⏰ Занятия:\n"
for event in events:
end_time = calculate_end_time(event.start_time, event.duration_min)
text += f" {event.start_time}-{end_time} {event.title}\n"
if not all_tasks and not events:
text += "На этот день расписания нет.\n"
return text
def calculate_end_time(start_time: str, duration_min: int) -> str:
"""Вычислить время окончания"""
hour, minute = map(int, start_time.split(":"))
total_minutes = hour * 60 + minute + duration_min
end_hour = total_minutes // 60
end_minute = total_minutes % 60
return f"{end_hour:02d}:{end_minute:02d}"
async def show_main_menu(message):
"""Показать главное меню с кнопками"""
keyboard = [
[InlineKeyboardButton("📅 Сегодня", callback_data="schedule_today")],
[InlineKeyboardButton("📅 Завтра", callback_data="schedule_tomorrow")],
[InlineKeyboardButton(" Добавить занятие", callback_data="add_event")],
[InlineKeyboardButton(" Добавить задачу", callback_data="add_task")],
[InlineKeyboardButton("🗑 Удалить", callback_data="delete_start")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await message.reply_text(
"Выберите действие:",
reply_markup=reply_markup
)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /start"""
await register_user(str(update.effective_chat.id))
await show_main_menu(update.message)
async def today_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /today"""
await register_user(str(update.effective_chat.id))
today = datetime.now(TZ).date()
date_str = today.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str, add_prefix=True)
await update.message.reply_text(text)
async def nextday_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /nextday"""
await register_user(str(update.effective_chat.id))
today = datetime.now(TZ).date()
tomorrow = today + timedelta(days=1)
date_str = tomorrow.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str, add_prefix=True)
await update.message.reply_text(text)
async def addtask_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /addtask"""
await register_user(str(update.effective_chat.id))
keyboard = [
[InlineKeyboardButton(" Добавить занятие", callback_data="add_event")],
[InlineKeyboardButton(" Добавить задачу", callback_data="add_task")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"Что вы хотите добавить?",
reply_markup=reply_markup
)
async def deltask_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /deltask"""
await register_user(str(update.effective_chat.id))
chat_id = str(update.effective_chat.id)
user_states[chat_id] = {"action": "delete"}
await show_date_selection_for_delete(update.message)
return SELECTING_DELETE_DATE
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик кнопок"""
query = update.callback_query
await query.answer()
chat_id = str(query.from_user.id)
today = datetime.now(TZ).date()
if query.data == "schedule_today":
# Расписание на сегодня (аналог /today) - отправляем новое сообщение
date_str = today.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str, add_prefix=True)
await query.message.reply_text(text)
elif query.data == "schedule_tomorrow":
# Расписание на завтра (аналог /nextday) - отправляем новое сообщение
tomorrow = today + timedelta(days=1)
date_str = tomorrow.strftime("%Y-%m-%d")
text = await get_schedule_for_date(date_str, add_prefix=True)
await query.message.reply_text(text)
elif query.data == "delete_start":
# Начало процесса удаления (аналог /deltask)
user_states[chat_id] = {"action": "delete"}
await show_date_selection_for_delete(query)
return SELECTING_DELETE_DATE
elif query.data == "add_event":
# Начинаем процесс добавления event
user_states[chat_id] = {"action": "add_event", "step": "date"}
await show_date_selection(query, "event")
return SELECTING_DATE
elif query.data == "add_task":
# Начинаем процесс добавления task
user_states[chat_id] = {"action": "add_task", "step": "date"}
await show_date_selection(query, "task")
return SELECTING_TASK_DATE
elif query.data.startswith("del_date_"):
# Выбрана дата для удаления
date_str = query.data.replace("del_date_", "")
if chat_id not in user_states:
user_states[chat_id] = {}
user_states[chat_id]["delete_date"] = date_str
await show_items_for_deletion(query, date_str)
return SELECTING_DELETE_ITEM
elif query.data.startswith("date_"):
# Выбрана дата
date_str = query.data.replace("date_", "")
if chat_id not in user_states:
user_states[chat_id] = {}
user_states[chat_id]["date"] = date_str
if user_states[chat_id].get("action") == "add_event":
await show_hour_selection(query)
return SELECTING_HOUR
else: # add_task
await query.edit_message_text("Введите название задачи:")
return ENTERING_TASK_TITLE
elif query.data.startswith("hour_"):
hour = query.data.replace("hour_", "")
user_states[chat_id]["hour"] = hour
await show_minute_selection(query)
return SELECTING_MINUTE
elif query.data.startswith("minute_"):
minute = query.data.replace("minute_", "")
user_states[chat_id]["minute"] = minute
await show_duration_selection(query)
return SELECTING_DURATION_HOUR
elif query.data.startswith("dur_hour_"):
dur_hour = int(query.data.replace("dur_hour_", ""))
user_states[chat_id]["duration_hour"] = dur_hour
await show_duration_minute_selection(query)
return SELECTING_DURATION_MIN
elif query.data.startswith("dur_min_"):
dur_min = int(query.data.replace("dur_min_", ""))
user_states[chat_id]["duration_minute"] = dur_min
await query.edit_message_text("Введите название занятия:")
return ENTERING_TITLE
elif query.data.startswith("delete_"):
# Удаление элемента (формат: delete_ID_kind)
parts = query.data.replace("delete_", "").split("_")
if len(parts) >= 2:
item_id = int(parts[0])
kind = parts[1]
await delete_item_by_id(query, item_id, kind)
if chat_id in user_states:
del user_states[chat_id]
return ConversationHandler.END
elif query.data.startswith("delete_scope_"):
# Удаление weekly task с указанием области
parts = query.data.replace("delete_scope_", "").split("_")
if len(parts) >= 3:
item_id = int(parts[0])
kind = parts[1]
scope = parts[2] # "one" или "series"
await delete_item_by_scope(query, item_id, kind, scope)
if chat_id in user_states:
del user_states[chat_id]
return ConversationHandler.END
# Возвращаемся в главное меню после завершения операции
await show_main_menu(query.message)
return ConversationHandler.END
async def show_date_selection(query, kind="event"):
"""Показать выбор даты"""
today = datetime.now(TZ).date()
monday = today - timedelta(days=today.weekday())
next_monday = monday + timedelta(days=7)
keyboard = []
weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
# Текущая неделя
for i in range(7):
date = monday + timedelta(days=i)
date_str = date.strftime("%Y-%m-%d")
label = f"{weekdays[i]} {date.day}"
keyboard.append([InlineKeyboardButton(label, callback_data=f"date_{date_str}")])
# Следующая неделя
for i in range(7):
date = next_monday + timedelta(days=i)
date_str = date.strftime("%Y-%m-%d")
label = f"{weekdays[i]} {date.day}"
keyboard.append([InlineKeyboardButton(label, callback_data=f"date_{date_str}")])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите дату:", reply_markup=reply_markup)
async def show_date_selection_for_delete(query_or_message):
"""Показать выбор даты для удаления"""
today = datetime.now(TZ).date()
monday = today - timedelta(days=today.weekday())
next_monday = monday + timedelta(days=7)
keyboard = []
weekdays = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
# Текущая неделя
for i in range(7):
date = monday + timedelta(days=i)
date_str = date.strftime("%Y-%m-%d")
label = f"{weekdays[i]} {date.day}"
keyboard.append([InlineKeyboardButton(label, callback_data=f"del_date_{date_str}")])
# Следующая неделя
for i in range(7):
date = next_monday + timedelta(days=i)
date_str = date.strftime("%Y-%m-%d")
label = f"{weekdays[i]} {date.day}"
keyboard.append([InlineKeyboardButton(label, callback_data=f"del_date_{date_str}")])
reply_markup = InlineKeyboardMarkup(keyboard)
# Проверяем, это query или message
if hasattr(query_or_message, 'edit_message_text'):
await query_or_message.edit_message_text("Выберите дату для удаления:", reply_markup=reply_markup)
else:
await query_or_message.reply_text("Выберите дату для удаления:", reply_markup=reply_markup)
async def show_hour_selection(query):
"""Показать выбор часа"""
keyboard = []
for hour in range(8, 21):
keyboard.append([InlineKeyboardButton(f"{hour:02d}:00", callback_data=f"hour_{hour:02d}")])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите час начала:", reply_markup=reply_markup)
async def show_minute_selection(query):
"""Показать выбор минут"""
keyboard = [
[InlineKeyboardButton("00", callback_data="minute_00")],
[InlineKeyboardButton("15", callback_data="minute_15")],
[InlineKeyboardButton("30", callback_data="minute_30")],
[InlineKeyboardButton("45", callback_data="minute_45")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите минуты:", reply_markup=reply_markup)
async def show_duration_selection(query):
"""Показать выбор длительности (часы)"""
keyboard = []
for hour in range(0, 4):
keyboard.append([InlineKeyboardButton(f"{hour} ч", callback_data=f"dur_hour_{hour}")])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите длительность (часы):", reply_markup=reply_markup)
async def show_duration_minute_selection(query):
"""Показать выбор длительности (минуты)"""
keyboard = [
[InlineKeyboardButton("00", callback_data="dur_min_00")],
[InlineKeyboardButton("15", callback_data="dur_min_15")],
[InlineKeyboardButton("30", callback_data="dur_min_30")],
[InlineKeyboardButton("45", callback_data="dur_min_45")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите длительность (минуты):", reply_markup=reply_markup)
async def handle_event_title(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработка ввода названия event"""
chat_id = str(update.effective_user.id)
title = update.message.text
state = user_states.get(chat_id, {})
if not state:
await update.message.reply_text("Ошибка: сессия истекла. Начните заново.")
return ConversationHandler.END
date = state.get("date")
hour = state.get("hour")
minute = state.get("minute")
dur_hour = state.get("duration_hour", 0)
dur_min = state.get("duration_minute", 0)
if not all([date, hour, minute]):
await update.message.reply_text("Ошибка: не все данные заполнены. Начните заново.")
if chat_id in user_states:
del user_states[chat_id]
return ConversationHandler.END
duration_min = dur_hour * 60 + dur_min
if duration_min == 0:
duration_min = 15 # Минимум 15 минут
start_time = f"{hour}:{minute}"
# Создаём event
async with AsyncSessionLocal() as db:
# Проверка пересечений
overlap = await check_event_overlap(db, date, start_time, duration_min, None)
if overlap:
await update.message.reply_text("❌ Нельзя добавить: пересечение по времени")
if chat_id in user_states:
del user_states[chat_id]
return ConversationHandler.END
event = Event(
date=date,
start_time=start_time,
duration_min=duration_min,
title=title
)
db.add(event)
await db.commit()
await update.message.reply_text(f"✅ Занятие добавлено: {date} {start_time} - {title}")
if chat_id in user_states:
del user_states[chat_id]
return ConversationHandler.END
async def handle_task_title(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработка ввода названия task"""
chat_id = str(update.effective_user.id)
title = update.message.text
state = user_states.get(chat_id, {})
state["title"] = title
keyboard = [
[InlineKeyboardButton("Да, повторять каждую неделю", callback_data="repeat_yes")],
[InlineKeyboardButton("Нет, только на эту дату", callback_data="repeat_no")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
"Повторять эту задачу каждую неделю?",
reply_markup=reply_markup
)
return SELECTING_REPEAT
async def handle_repeat_choice(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработка выбора повторения"""
query = update.callback_query
await query.answer()
chat_id = str(query.from_user.id)
state = user_states.get(chat_id, {})
if not state:
await query.edit_message_text("Ошибка: сессия истекла. Начните заново.")
return ConversationHandler.END
if query.data == "repeat_yes":
state["repeat_weekly"] = True
# Создаём weekly task
async with AsyncSessionLocal() as db:
weekday = get_weekday_from_date(state["date"])
task = Task(
date=state["date"],
title=state["title"],
repeat_weekly=True,
weekday=weekday
)
db.add(task)
await db.commit()
await query.edit_message_text(f"✅ Задача добавлена (повторяется каждую неделю): {state['title']}")
else:
state["repeat_weekly"] = False
# Создаём разовую task
async with AsyncSessionLocal() as db:
task = Task(
date=state["date"],
title=state["title"],
repeat_weekly=False
)
db.add(task)
await db.commit()
await query.edit_message_text(f"✅ Задача добавлена: {state['title']}")
del user_states[chat_id]
return ConversationHandler.END
async def show_items_for_deletion(query, date_str: str):
"""Показать список задач и занятий для удаления"""
async with AsyncSessionLocal() as db:
from backend.utils import materialize_weekly_tasks
# Получаем tasks (разовые)
result = await db.execute(
select(Task).where(
and_(Task.date == date_str, Task.repeat_weekly == False)
)
)
tasks = result.scalars().all()
# Получаем weekly tasks для этой даты
weekly_items = await materialize_weekly_tasks(db, date_str, date_str)
weekly_tasks = [item for item in weekly_items if item.kind == "task"]
# Получаем events
result = await db.execute(
select(Event).where(Event.date == date_str)
)
events = result.scalars().all()
events = sorted(events, key=lambda e: e.start_time)
keyboard = []
items_list = []
# Добавляем tasks
for task in tasks:
items_list.append(("task", task.id, task.title, None))
keyboard.append([InlineKeyboardButton(f"📋 {task.title}", callback_data=f"delete_{task.id}_task")])
# Добавляем weekly tasks
for task_item in weekly_tasks:
# Для weekly tasks нужно найти оригинальную задачу
result = await db.execute(
select(Task).where(Task.id == task_item.id)
)
task = result.scalar_one_or_none()
if task:
items_list.append(("task", task.id, task.title, None))
keyboard.append([InlineKeyboardButton(f"📋 {task.title} (повторяется)", callback_data=f"delete_{task.id}_task")])
# Добавляем events
for event in events:
end_time = calculate_end_time(event.start_time, event.duration_min)
items_list.append(("event", event.id, event.title, event.start_time))
keyboard.append([InlineKeyboardButton(f"{event.start_time}-{end_time} {event.title}", callback_data=f"delete_{event.id}_event")])
if not keyboard:
await query.edit_message_text(f"На {format_date_russian(date_str)} нет записей для удаления.")
return ConversationHandler.END
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"Выберите запись для удаления на {format_date_russian(date_str)}:",
reply_markup=reply_markup
)
async def delete_item_by_id(query, item_id: int, kind: str):
"""Удалить элемент по ID"""
async with AsyncSessionLocal() as db:
if kind == "task":
result = await db.execute(select(Task).where(Task.id == item_id))
task = result.scalar_one_or_none()
if task:
if task.repeat_weekly:
# Для weekly task нужно спросить область применения
chat_id = str(query.from_user.id)
if chat_id not in user_states:
user_states[chat_id] = {}
user_states[chat_id]["delete_item_id"] = item_id
user_states[chat_id]["delete_kind"] = "task"
user_states[chat_id]["delete_date"] = user_states[chat_id].get("delete_date", "")
keyboard = [
[InlineKeyboardButton("Только на эту дату", callback_data=f"delete_scope_{item_id}_task_one")],
[InlineKeyboardButton("Для всего ряда", callback_data=f"delete_scope_{item_id}_task_series")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(
f"Задача '{task.title}' повторяется каждую неделю. Что удалить?",
reply_markup=reply_markup
)
return SELECTING_DELETE_ITEM
else:
await db.delete(task)
await db.commit()
await query.edit_message_text(f"✅ Задача '{task.title}' удалена.")
return ConversationHandler.END
elif kind == "event":
result = await db.execute(select(Event).where(Event.id == item_id))
event = result.scalar_one_or_none()
if event:
title = event.title
await db.delete(event)
await db.commit()
await query.edit_message_text(f"✅ Занятие '{title}' удалено.")
return ConversationHandler.END
await query.edit_message_text("❌ Запись не найдена.")
return ConversationHandler.END
async def delete_item_by_scope(query, item_id: int, kind: str, scope: str):
"""Удалить элемент с указанием области (для weekly tasks)"""
async with AsyncSessionLocal() as db:
if kind == "task":
result = await db.execute(select(Task).where(Task.id == item_id))
task = result.scalar_one_or_none()
if task and task.repeat_weekly:
chat_id = str(query.from_user.id)
state = user_states.get(chat_id, {})
date_str = state.get("delete_date", "")
if scope == "one":
# Создаем исключение на удаление для конкретной даты
from backend.database import WeeklyTaskException
weekday = get_weekday_from_date(date_str)
exception = WeeklyTaskException(
weekday=weekday,
date=date_str,
action="delete"
)
db.add(exception)
await db.commit()
await query.edit_message_text(f"✅ Задача '{task.title}' удалена только на {format_date_russian(date_str)}.")
elif scope == "series":
# Удаляем весь ряд weekly tasks
await db.execute(
Task.__table__.delete().where(Task.weekday == task.weekday)
)
await db.commit()
await query.edit_message_text(f"✅ Задача '{task.title}' удалена для всего ряда.")
return ConversationHandler.END
await query.edit_message_text("❌ Ошибка при удалении.")
return ConversationHandler.END
async def send_reminders():
"""Отправка напоминаний о событиях"""
while True:
try:
now = datetime.now(TZ)
target_time = now + timedelta(minutes=5)
target_date = target_time.date()
target_time_str = target_time.strftime("%H:%M")
async with AsyncSessionLocal() as db:
# Находим события, которые начинаются через 5 минут
result = await db.execute(
select(Event).where(
and_(
Event.date == target_date.strftime("%Y-%m-%d"),
Event.start_time == target_time_str
)
)
)
events = result.scalars().all()
if events:
# Получаем всех пользователей
result = await db.execute(select(TelegramUser))
users = result.scalars().all()
for event in events:
message = f"🔔 Напоминание: через 5 минут начинается {event.title} ({event.start_time})"
for user in users:
try:
await application.bot.send_message(
chat_id=user.chat_id,
text=message
)
except Exception as e:
logger.error(f"Error sending reminder to {user.chat_id}: {e}")
except Exception as e:
logger.error(f"Error in reminder loop: {e}")
await asyncio.sleep(60) # Проверяем каждую минуту
application = None
async def start_bot():
"""Запустить бота"""
global application
if not BOT_TOKEN:
logger.warning("TELEGRAM_BOT_TOKEN not set, bot will not start")
return
application = Application.builder().token(BOT_TOKEN).build()
# Conversation handler для добавления event
event_conv = ConversationHandler(
entry_points=[CallbackQueryHandler(button_handler, pattern="^add_event$")],
states={
SELECTING_DATE: [CallbackQueryHandler(button_handler, pattern="^date_")],
SELECTING_HOUR: [CallbackQueryHandler(button_handler, pattern="^hour_")],
SELECTING_MINUTE: [CallbackQueryHandler(button_handler, pattern="^minute_")],
SELECTING_DURATION_HOUR: [CallbackQueryHandler(button_handler, pattern="^dur_hour_")],
SELECTING_DURATION_MIN: [CallbackQueryHandler(button_handler, pattern="^dur_min_")],
ENTERING_TITLE: [MessageHandler(filters.TEXT & ~filters.COMMAND, handle_event_title)],
},
fallbacks=[],
name="event_conv",
)
# Conversation handler для добавления task
task_conv = ConversationHandler(
entry_points=[CallbackQueryHandler(button_handler, pattern="^add_task$")],
states={
SELECTING_TASK_DATE: [CallbackQueryHandler(button_handler, pattern="^date_")],
ENTERING_TASK_TITLE: [MessageHandler(filters.TEXT & ~filters.COMMAND, handle_task_title)],
SELECTING_REPEAT: [CallbackQueryHandler(handle_repeat_choice, pattern="^repeat_")],
},
fallbacks=[],
name="task_conv",
)
# Команды
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("today", today_command))
application.add_handler(CommandHandler("nextday", nextday_command))
application.add_handler(CommandHandler("addtask", addtask_command))
# Conversation handler для удаления
delete_conv = ConversationHandler(
entry_points=[CommandHandler("deltask", deltask_command)],
states={
SELECTING_DELETE_DATE: [CallbackQueryHandler(button_handler, pattern="^del_date_")],
SELECTING_DELETE_ITEM: [CallbackQueryHandler(button_handler, pattern="^(delete_|delete_scope_)")],
},
fallbacks=[],
name="delete_conv",
)
application.add_handler(delete_conv)
application.add_handler(event_conv)
application.add_handler(task_conv)
application.add_handler(CallbackQueryHandler(button_handler))
# Запуск напоминаний
asyncio.create_task(send_reminders())
# Запускаем бота в фоне
await application.initialize()
await application.start()
await application.updater.start_polling(drop_pending_updates=True)