#!/usr/bin/env python3 # bot.py import asyncio import sys import warnings from datetime import datetime, timedelta from zoneinfo import ZoneInfo from telegram import InlineKeyboardMarkup, InlineKeyboardButton from telegram.ext import Application, ContextTypes # Импорты из наших модулей from config import * from database import init_db from app.handlers import help_handler, unknown_command, callback_taken from conversations import * from app.utils import main_menu_kb # Отключаем предупреждения PTB warnings.filterwarnings("ignore", category=UserWarning) # Настройка логирования logger = setup_logging() # ----------------- BACKGROUND JOB ----------------- async def background_job(context: ContextTypes.DEFAULT_TYPE): """Задача для отправки напоминаний через JobQueue""" try: now_utc_dt = datetime.utcnow() now_min_utc = utc_str_min(now_utc_dt) conn = get_db_connection() cursor = conn.cursor() cursor.execute(''' SELECT pt.id, pt.pill_id, pt.time, pt.last_sent, pt.last_confirmed, p.user_id, p.name, p.dose, p.interval_days, p.weekdays, p.course_days, p.start_date, u.tz FROM pill_times pt JOIN pills p ON p.id = pt.pill_id JOIN users u ON u.id = p.user_id WHERE p.course_days = 0 OR (julianday(?) - julianday(p.start_date)) < p.course_days ''', (now_utc_dt.strftime("%Y-%m-%d"),)) rows = cursor.fetchall() for row in rows: ptid = row["id"] pid = row["pill_id"] time_hhmm = row["time"] last_sent = row["last_sent"] last_confirmed = row["last_confirmed"] user_id = row["user_id"] pill_name = row["name"] pill_dose = row["dose"] interval_days = row["interval_days"] weekdays_csv = row["weekdays"] course_days = row["course_days"] start_date = row["start_date"] user_tz = row["tz"] or "UTC" try: tz = ZoneInfo(user_tz) except Exception: tz = ZoneInfo("UTC") user_local_now = datetime.now(tz) user_local_hhmm = user_local_now.strftime("%H:%M") if user_local_hhmm != time_hhmm: continue if weekdays_csv: wlist = weekdays_csv_to_list(weekdays_csv) if user_local_now.weekday() not in wlist: continue else: if interval_days and interval_days > 1 and start_date: try: sd = datetime.strptime(start_date, "%Y-%m-%d").date() diff = (user_local_now.date() - sd).days if diff % interval_days != 0: continue except Exception: pass if last_sent == now_min_utc: continue if last_confirmed and last_sent and last_confirmed >= last_sent: continue kb = InlineKeyboardMarkup([[ InlineKeyboardButton("✅ Выпил(-а)", callback_data=f"take:{ptid}") ]]) try: await context.bot.send_message( chat_id=user_id, text=f"💊 Пора принять {pill_name} ({pill_dose})", reply_markup=kb ) cursor.execute( 'UPDATE pill_times SET last_sent=? WHERE id=?', (now_min_utc, ptid) ) conn.commit() logger.info(f"Sent reminder to user {user_id} for pill {pill_name}") except Exception as e: logger.error(f"Failed to send reminder to user {user_id}: {e}") conn.close() except Exception as e: logger.exception(f"Error in background job: {e}") # ----------------- SETUP HANDLERS ----------------- def setup_handlers(application: Application): """Настройка всех обработчиков""" # Главный ConversationHandler main_conv = ConversationHandler( entry_points=[ CommandHandler("start", start_handler), CommandHandler("help", help_handler), MessageHandler(filters.Regex(f'^{BTN_LIST}$'), main_menu_handler), MessageHandler(filters.Regex(f'^{BTN_HISTORY}$'), main_menu_handler), MessageHandler(filters.Regex(f'^{BTN_ADD}$'), main_menu_handler), MessageHandler(filters.Regex(f'^{BTN_EDIT}$'), main_menu_handler), MessageHandler(filters.Regex(f'^{BTN_DELETE}$'), main_menu_handler), MessageHandler(filters.Regex(f'^{BTN_TZ}$'), main_menu_handler), MessageHandler(filters.Regex(f'^{BTN_CANCEL}$'), lambda u,c: u.message.reply_text("Отмена.", reply_markup=main_menu_kb())), ], states={ S_MAIN_MENU: [ MessageHandler(filters.TEXT & ~filters.COMMAND, main_menu_handler) ], S_TZ_SELECT: [ MessageHandler( filters.Regex(f'^({"|".join(TZ_SUGGESTIONS)})$'), tz_select_handler ), MessageHandler( filters.Regex("^Ввести вручную$"), tz_select_handler ), ], S_TZ_MANUAL: [ MessageHandler(filters.TEXT & ~filters.COMMAND, tz_manual_input_handler) ], S_GENDER: [ MessageHandler(filters.Regex("^(Я мужчина|Я женщина)$"), gender_handler) ], S_NAME: [ MessageHandler(filters.TEXT & ~filters.COMMAND, name_handler) ], S_ADD_NAME: [ MessageHandler(filters.TEXT & ~filters.COMMAND, add_name) ], S_ADD_DOSE: [ MessageHandler(filters.TEXT & ~filters.COMMAND, add_dose) ], S_ADD_FREQ_TYPE: [ MessageHandler(filters.TEXT & ~filters.COMMAND, add_freq_type) ], S_ADD_FREQ_N: [ MessageHandler(filters.TEXT & ~filters.COMMAND, add_freq_n) ], S_ADD_WEEKDAYS: [ CallbackQueryHandler(weekday_callback, pattern="^(wd_toggle:|wd_done|wd_cancel)$") ], S_ADD_TIMES: [ MessageHandler(filters.TEXT & ~filters.COMMAND, add_times_handler) ], S_ADD_COURSE: [ MessageHandler(filters.TEXT & ~filters.COMMAND, add_course_handler) ], S_EDIT_SELECT: [ MessageHandler(filters.TEXT & ~filters.COMMAND, edit_select_handler) ], S_EDIT_FIELD: [ MessageHandler(filters.TEXT & ~filters.COMMAND, edit_field_choice) ], S_EDIT_VALUE: [ MessageHandler(filters.TEXT & ~filters.COMMAND, edit_value_handler), CallbackQueryHandler(weekday_callback, pattern="^(wd_toggle:|wd_done|wd_cancel)$") ], S_DELETE_SELECT: [ MessageHandler(filters.TEXT & ~filters.COMMAND, delete_confirm) ], }, fallbacks=[ CommandHandler("start", start_handler), CommandHandler("help", help_handler), MessageHandler(filters.Regex(f'^{BTN_CANCEL}$'), lambda u,c: u.message.reply_text("Отмена.", reply_markup=main_menu_kb())), ], per_message=True ) application.add_handler(main_conv) # Обработчик callback для принятия лекарства application.add_handler(CallbackQueryHandler(callback_taken, pattern=r"^take:\d+$")) # Обработчик неизвестных команд application.add_handler(MessageHandler(filters.COMMAND, unknown_command)) # Общий обработчик текста для необработанных сообщений application.add_handler(MessageHandler( filters.TEXT & ~filters.COMMAND, lambda u,c: u.message.reply_text("Пожалуйста, используйте кнопки меню.", reply_markup=main_menu_kb()) )) # ----------------- MAIN FUNCTION ----------------- def main(): """Основная функция запуска бота""" logger.info(f"Starting Pill Reminder Bot") logger.info(f"Database: {DB_FILE}") logger.info(f"Log level: {LOG_LEVEL}") # Инициализация БД init_db() # Создаем приложение application = Application.builder().token(TOKEN).build() # Настраиваем обработчики setup_handlers(application) # Настраиваем фоновую задачу через JobQueue job_queue = application.job_queue if job_queue: # Запускаем задачу каждую минуту job_queue.run_repeating( background_job, interval=60, # 60 секунд first=10 # Запустить через 10 секунд после старта ) logger.info("Background job scheduled via JobQueue") # Запускаем бота application.run_polling() if __name__ == "__main__": main()